Merge branch 'next-minor' into stable

This commit is contained in:
Dylan K. Taylor 2023-01-30 21:52:41 +00:00
commit 3dd1a14fb7
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
89 changed files with 1988 additions and 749 deletions

View File

@ -36,8 +36,8 @@ use function ksort;
use function mb_strtoupper;
use function preg_match;
use function sprintf;
use function str_ends_with;
use function str_replace;
use function substr;
use const SORT_STRING;
use const STDERR;
@ -121,7 +121,7 @@ require dirname(__DIR__) . '/vendor/autoload.php';
if(is_dir($argv[1])){
/** @var string $file */
foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($argv[1], \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_PATHNAME)) as $file){
if(substr($file, -4) !== ".php"){
if(!str_ends_with($file, ".php")){
continue;
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\build\make_release;
use pocketmine\utils\Filesystem;
use pocketmine\utils\Utils;
use pocketmine\utils\VersionString;
use pocketmine\VersionInfo;
@ -30,7 +31,6 @@ use function array_keys;
use function array_map;
use function dirname;
use function fgets;
use function file_get_contents;
use function file_put_contents;
use function fwrite;
use function getopt;
@ -50,7 +50,7 @@ use const STR_PAD_LEFT;
require_once dirname(__DIR__) . '/vendor/autoload.php';
function replaceVersion(string $versionInfoPath, string $newVersion, bool $isDev, string $channel) : void{
$versionInfo = Utils::assumeNotFalse(file_get_contents($versionInfoPath), $versionInfoPath . " should always exist");
$versionInfo = Filesystem::fileGetContents($versionInfoPath);
$versionInfo = preg_replace(
$pattern = '/^([\t ]*public )?const BASE_VERSION = "(\d+)\.(\d+)\.(\d+)(?:-(.*))?";$/m',
'$1const BASE_VERSION = "' . $newVersion . '";',

94
changelogs/4.13-beta.md Normal file
View File

@ -0,0 +1,94 @@
**For Minecraft: Bedrock Edition 1.19.50**
This is a minor feature release for PocketMine-MP, introducing some new features and improvements.
### Note about API versions
Plugins which don't touch the protocol and compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
Plugin developers should **only** update their required API to this version if you need the changes in this build.
**WARNING: If your plugin uses the protocol, you're not shielded by API change constraints.** You should consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you do.
# 4.13.0-BETA1
Released 18th January 2023.
## Gameplay
- Death message is now shown on the death screen when a player dies.
- Armour damage is now only increased if the armour reduced the damage taken.
- Implemented Swift Sneak enchantment.
- Fixed incorrect collision box calculation of walls and glass/bars when connected. Note: Client-side, wall connections are still broken; this only fixes projectile flight server-side.
## Performance
- Improved performance of chunk selection for chunk random ticking using a cache. This improves performance of chunk random ticking by 10-20%.
## Localization
- Added localized description for the `/dumpmemory` command.
## Permissions
- Added the following new core permissions:
- `pocketmine.command.effect.other` - allows the player to use the `/effect` command on other players (default operator only)
- `pocketmine.command.effect.self` - allows the player to use the `/effect` command on themselves (default operator only)
- `pocketmine.command.enchant.other` - allows the player to use the `/enchant` command on other players (default operator only)
- `pocketmine.command.enchant.self` - allows the player to use the `/enchant` command on themselves (default operator only)
- `pocketmine.command.gamemode.other` - allows the player to use the `/gamemode` command on other players (default operator only)
- `pocketmine.command.gamemode.self` - allows the player to use the `/gamemode` command on themselves (default operator only)
- `pocketmine.command.give.other` - allows the player to use the `/give` command on other players (default operator only)
- `pocketmine.command.give.self` - allows the player to use the `/give` command on themselves (default operator only)
- `pocketmine.command.spawnpoint.other` - allows the player to use the `/spawnpoint` command on other players (default operator only)
- `pocketmine.command.spawnpoint.self` - allows the player to use the `/spawnpoint` command on themselves (default operator only)
- `pocketmine.command.teleport.other` - allows the player to use the `/teleport` command on other players (default operator only)
- `pocketmine.command.teleport.self` - allows the player to use the `/teleport` command on themselves (default operator only)
- `pocketmine.command.title.other` - allows the player to use the `/title` command on other players (default operator only)
- `pocketmine.command.title.self` - allows the player to use the `/title` command on themselves (default operator only)
## Internals
- Decoupled `Player->sendMessage()` and `Player->sendTranslation()`.
- Refactored resource pack loading in `ResourcePackManager` to make it easier to understand.
- Client-aware translation processing has been moved to `NetworkSession` due to being client-specific.
- Replaced hardcoded strings with constants in various places.
- `NetworkSession` destructive cleanup is now deferred to the next session tick. This fixes various `InventoryManager` crashes when kicking players during events.
- Updated code using `strpos()` to use `str_starts_with()`, `str_ends_with()` and `str_contains()` where appropriate.
- Added documentation for some internal methods.
## API
### `pocketmine\command`
- The following new API methods have been added:
- `protected VanillaCommand->fetchPermittedPlayerTarget(...) : ?Player` - fetches a player target according to the given sender permissions, or null if not found or not permitted
### `pocketmine\entity`
- The following new API methods have been added:
- `public Living->getDisplayName() : string` - the name of the entity to be shown in death messages, commands etc.
### `pocketmine\event\world`
- The following new classes have been added:
- `WorldSoundEvent` - called when a sound is played in a world
- `WorldParticleEvent` - called when a particle is spawned in a world
### `pocketmine\item`
- The following new API methods have been added:
- `public Item->onInteractEntity(Player $player, Entity $entity, Vector3 $clickVector) : bool` - called when a player interacts with an entity with this item in hand
### `pocketmine\lang`
- `Language->translate()` and `Language->translateString()` no longer parse nested translation in the "base text". This was never intended behaviour, and didn't work beyond the first level anyway.
### `pocketmine\player`
- The following new interfaces have been added:
- `PlayerDataProvider` - implemented by classes which want to offer storage for player data
- The following new classes have been added:
- `DatFilePlayerDataProvider` - the default player data provider, which stores `.dat` files in the `players` folder
- `PlayerDataLoadException` - thrown when an error occurs while loading player data
- `PlayerDataSaveException` - thrown when an error occurs while saving player data
- The following API methods have been deprecated:
- `Player->sendTranslation()` - use `Player->sendMessage()` instead with a `Translatable` message
### `pocketmine\resourcepacks`
- The following new API methods have been added:
- `public ResourcePackManager->setResourceStack(list<ResourcePack> $resourceStack) : void` - sets the list of resource packs to be applied by players
- `public ResourcePackManager->setPackEncryptionKey(string $id, ?string $key) : void` - sets the encryption key to be used for a resource pack
### `pocketmine\utils`
- The following new API methods have been added:
- `public static Filesystem::fileGetContents(...) : string` - a wrapper around `file_get_contents()` which throws an exception on failure
### `pocketmine\world`
- The following new API methods have been added:
- `public World->requestSafeSpawn(?Vector3 $spawn = null) : Promise<Position>` - an async version of `getSafeSpawn()` which generates all the needed chunks before returning

View File

@ -35,13 +35,13 @@
"fgrosse/phpasn1": "^2.3",
"netresearch/jsonmapper": "^4.0",
"pocketmine/bedrock-data": "~1.13.0+bedrock-1.19.50",
"pocketmine/bedrock-protocol": "~17.1.0+bedrock-1.19.50",
"pocketmine/bedrock-protocol": "~18.1.0+bedrock-1.19.50",
"pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "^1.0.2",
"pocketmine/classloader": "^0.2.0",
"pocketmine/color": "^0.2.0",
"pocketmine/color": "^0.3.0",
"pocketmine/errorhandler": "^0.6.0",
"pocketmine/locale-data": "~2.11.0",
"pocketmine/locale-data": "~2.18.0",
"pocketmine/log": "^0.4.0",
"pocketmine/log-pthreads": "^0.4.0",
"pocketmine/math": "^0.4.0",

48
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8e2b03d128d6c14cf0f5e78d4bb7af90",
"content-hash": "2c0f273b515174abfdcef4fc4ad3c262",
"packages": [
{
"name": "adhocore/json-comment",
@ -276,16 +276,16 @@
},
{
"name": "pocketmine/bedrock-protocol",
"version": "17.1.0+bedrock-1.19.50",
"version": "18.1.0+bedrock-1.19.50",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockProtocol.git",
"reference": "c572706cf5e3202718dd35a35dd30fe08cd671de"
"reference": "c34f22847a1cc362a3f1c45698576d30d1e8392f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/c572706cf5e3202718dd35a35dd30fe08cd671de",
"reference": "c572706cf5e3202718dd35a35dd30fe08cd671de",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/c34f22847a1cc362a3f1c45698576d30d1e8392f",
"reference": "c34f22847a1cc362a3f1c45698576d30d1e8392f",
"shasum": ""
},
"require": {
@ -293,13 +293,13 @@
"netresearch/jsonmapper": "^4.0",
"php": "^8.0",
"pocketmine/binaryutils": "^0.2.0",
"pocketmine/color": "^0.2.0",
"pocketmine/color": "^0.2.0 || ^0.3.0",
"pocketmine/math": "^0.3.0 || ^0.4.0",
"pocketmine/nbt": "^0.3.0",
"ramsey/uuid": "^4.1"
},
"require-dev": {
"phpstan/phpstan": "1.9.3",
"phpstan/phpstan": "1.9.13",
"phpstan/phpstan-phpunit": "^1.0.0",
"phpstan/phpstan-strict-rules": "^1.0.0",
"phpunit/phpunit": "^9.5"
@ -317,9 +317,9 @@
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
"support": {
"issues": "https://github.com/pmmp/BedrockProtocol/issues",
"source": "https://github.com/pmmp/BedrockProtocol/tree/17.1.0+bedrock-1.19.50"
"source": "https://github.com/pmmp/BedrockProtocol/tree/18.1.0+bedrock-1.19.50"
},
"time": "2022-12-15T20:34:49+00:00"
"time": "2023-01-20T12:35:30+00:00"
},
{
"name": "pocketmine/binaryutils",
@ -460,24 +460,24 @@
},
{
"name": "pocketmine/color",
"version": "0.2.0",
"version": "0.3.0",
"source": {
"type": "git",
"url": "https://github.com/pmmp/Color.git",
"reference": "09be6ea6d76f2e33d6813c39d29c22c46c17e1d2"
"reference": "8cb346d0a21ad3287cc8d7175e4b643416607249"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/Color/zipball/09be6ea6d76f2e33d6813c39d29c22c46c17e1d2",
"reference": "09be6ea6d76f2e33d6813c39d29c22c46c17e1d2",
"url": "https://api.github.com/repos/pmmp/Color/zipball/8cb346d0a21ad3287cc8d7175e4b643416607249",
"reference": "8cb346d0a21ad3287cc8d7175e4b643416607249",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
"php": "^8.0"
},
"require-dev": {
"phpstan/phpstan": "0.12.59",
"phpstan/phpstan-strict-rules": "^0.12.2"
"phpstan/phpstan": "1.9.4",
"phpstan/phpstan-strict-rules": "^1.2.0"
},
"type": "library",
"autoload": {
@ -492,9 +492,9 @@
"description": "Color handling library used by PocketMine-MP and related projects",
"support": {
"issues": "https://github.com/pmmp/Color/issues",
"source": "https://github.com/pmmp/Color/tree/0.2.0"
"source": "https://github.com/pmmp/Color/tree/0.3.0"
},
"time": "2020-12-11T01:24:32+00:00"
"time": "2022-12-18T19:49:21+00:00"
},
{
"name": "pocketmine/errorhandler",
@ -537,16 +537,16 @@
},
{
"name": "pocketmine/locale-data",
"version": "2.11.0",
"version": "2.18.3",
"source": {
"type": "git",
"url": "https://github.com/pmmp/Language.git",
"reference": "4b33d8fa53eda53d9662a7478806ebae2e4a5c53"
"reference": "da25bfe9ee4822a84feb9b7e620c56ad4000aed0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/Language/zipball/4b33d8fa53eda53d9662a7478806ebae2e4a5c53",
"reference": "4b33d8fa53eda53d9662a7478806ebae2e4a5c53",
"url": "https://api.github.com/repos/pmmp/Language/zipball/da25bfe9ee4822a84feb9b7e620c56ad4000aed0",
"reference": "da25bfe9ee4822a84feb9b7e620c56ad4000aed0",
"shasum": ""
},
"type": "library",
@ -554,9 +554,9 @@
"description": "Language resources used by PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/Language/issues",
"source": "https://github.com/pmmp/Language/tree/2.11.0"
"source": "https://github.com/pmmp/Language/tree/2.18.3"
},
"time": "2022-11-25T14:24:34+00:00"
"time": "2023-01-17T21:43:36+00:00"
},
{
"name": "pocketmine/log",

View File

@ -69,6 +69,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 +117,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 +135,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);

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,6 +182,27 @@ 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 TimeTrackingSleeperHandler $tickSleeper;
@ -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,6 +768,8 @@ 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);
Timings::init();
$this->tickSleeper = new TimeTrackingSleeperHandler(Timings::$serverInterrupts);
@ -807,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);
}
@ -963,7 +953,7 @@ class Server{
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_license($this->getName())));
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();
@ -979,7 +969,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 +991,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 +1031,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);
}
@ -1821,11 +1813,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 +1829,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();
@ -1866,12 +1858,12 @@ class Server{
$now = microtime(true);
$totalTickTimeSeconds = $now - $tickTime + ($this->tickSleeper->getNotificationProcessingTime() / 1_000_000_000);
$this->currentTPS = min(20, 1 / max(0.001, $totalTickTimeSeconds));
$this->currentUse = min(1, $totalTickTimeSeconds / 0.05);
$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();
@ -1879,7 +1871,7 @@ class Server{
if(($this->nextTick - $tickTime) < -1){
$this->nextTick = $tickTime;
}else{
$this->nextTick += 0.05;
$this->nextTick += self::TARGET_SECONDS_PER_TICK;
}
}
}

View File

@ -31,9 +31,9 @@ use function str_repeat;
final class VersionInfo{
public const NAME = "PocketMine-MP";
public const BASE_VERSION = "4.12.12";
public const BASE_VERSION = "4.13.0-BETA2";
public const IS_DEVELOPMENT_BUILD = true;
public const BUILD_CHANNEL = "stable";
public const BUILD_CHANNEL = "beta";
private function __construct(){
//NOOP

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

@ -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

@ -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,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);
}
}
}

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

@ -77,6 +77,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 +134,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 +241,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 +290,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 +310,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 +320,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 +411,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 +447,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 +466,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())
);
}

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{
@ -195,20 +202,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

@ -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\BEDROCK_DATA_PATH, "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

@ -456,6 +456,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");
@ -573,28 +576,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);
}
@ -607,7 +619,6 @@ class NetworkSession{
if($this->player !== null){
$this->player->onPostDisconnect($reason, null);
}
$this->doServerDisconnect($reason, false);
}, $reason);
}
@ -616,21 +627,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.
@ -714,7 +714,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())){
@ -775,9 +775,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));
}
}
@ -852,7 +852,7 @@ class NetworkSession{
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_TELEPORT => $for->hasPermission(DefaultPermissionNames::COMMAND_TELEPORT_SELF),
UpdateAbilitiesPacketLayer::ABILITY_INVULNERABLE => $for->isCreative(),
UpdateAbilitiesPacketLayer::ABILITY_MUTED => false,
UpdateAbilitiesPacketLayer::ABILITY_WORLD_BUILDER => false,
@ -962,15 +962,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));
}
}
/**
@ -1139,6 +1143,11 @@ class NetworkSession{
}
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

@ -27,9 +27,9 @@ 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,9 +38,7 @@ 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{

View File

@ -26,10 +26,9 @@ namespace pocketmine\network\mcpe\convert;
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(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'required_item_list.json'));
$table = json_decode($data, true);
if(!is_array($table)){
throw new AssumptionFailedError("Invalid item list format");

View File

@ -26,11 +26,11 @@ namespace pocketmine\network\mcpe\convert;
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(Path::join(\pocketmine\BEDROCK_DATA_PATH, '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

@ -30,10 +30,9 @@ 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\Filesystem;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use Symfony\Component\Filesystem\Path;
use function file_get_contents;
/**
* @internal
@ -57,7 +56,7 @@ final class RuntimeBlockMapping{
public function __construct(string $canonicalBlockStatesFile, string $r12ToCurrentBlockMapFile){
$stream = PacketSerializer::decoder(
Utils::assumeNotFalse(file_get_contents($canonicalBlockStatesFile), "Missing required resource file"),
Filesystem::fileGetContents($canonicalBlockStatesFile),
0,
new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary())
);
@ -75,7 +74,7 @@ final class RuntimeBlockMapping{
/** @var R12ToCurrentBlockMapEntry[] $legacyStateMap */
$legacyStateMap = [];
$legacyStateMapReader = PacketSerializer::decoder(
Utils::assumeNotFalse(file_get_contents($r12ToCurrentBlockMapFile), "Missing required resource file"),
Filesystem::fileGetContents($r12ToCurrentBlockMapFile),
0,
new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary())
);

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;
@ -122,8 +123,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;
/**
@ -661,7 +662,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){
@ -732,7 +733,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;
}

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);

View File

@ -62,7 +62,7 @@ use function mkdir;
use function realpath;
use function shuffle;
use function sprintf;
use function strpos;
use function str_contains;
use function strtolower;
/**
@ -296,7 +296,7 @@ class PluginManager{
continue;
}
if(strpos($name, " ") !== false){
if(str_contains($name, " ")){
$this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_spacesDiscouraged($name)));
}

View File

@ -28,9 +28,8 @@ use function count;
use function file;
use function implode;
use function is_file;
use function strlen;
use function strpos;
use function substr;
use function str_contains;
use function str_ends_with;
use const FILE_IGNORE_NEW_LINES;
use const FILE_SKIP_EMPTY_LINES;
@ -41,8 +40,7 @@ use const FILE_SKIP_EMPTY_LINES;
class ScriptPluginLoader implements PluginLoader{
public function canLoadPlugin(string $path) : bool{
$ext = ".php";
return is_file($path) && substr($path, -strlen($ext)) === $ext;
return is_file($path) && str_ends_with($path, ".php");
}
/**
@ -66,7 +64,7 @@ class ScriptPluginLoader implements PluginLoader{
$docCommentLines = [];
foreach($content as $line){
if(!$insideHeader){
if(strpos($line, "/**") !== false){
if(str_contains($line, "/**")){
$insideHeader = true;
}else{
continue;
@ -75,7 +73,7 @@ class ScriptPluginLoader implements PluginLoader{
$docCommentLines[] = $line;
if(strpos($line, "*/") !== false){
if(str_contains($line, "*/")){
break;
}
}

View File

@ -23,14 +23,13 @@ declare(strict_types=1);
namespace pocketmine\resourcepacks;
use pocketmine\errorhandler\ErrorToExceptionHandler;
use pocketmine\utils\Config;
use pocketmine\utils\Filesystem;
use Symfony\Component\Filesystem\Path;
use function array_keys;
use function copy;
use function count;
use function file_exists;
use function file_get_contents;
use function gettype;
use function is_array;
use function is_dir;
@ -38,6 +37,8 @@ use function is_float;
use function is_int;
use function is_string;
use function mkdir;
use function rtrim;
use function strlen;
use function strtolower;
use const DIRECTORY_SEPARATOR;
@ -93,41 +94,24 @@ class ResourcePackManager{
}
$pack = (string) $pack;
try{
$packPath = Path::join($this->path, $pack);
if(!file_exists($packPath)){
throw new ResourcePackException("File or directory not found");
}
if(is_dir($packPath)){
throw new ResourcePackException("Directory resource packs are unsupported");
}
$newPack = $this->loadPackFromPath(Path::join($this->path, $pack));
$newPack = null;
//Detect the type of resource pack.
$info = new \SplFileInfo($packPath);
switch($info->getExtension()){
case "zip":
case "mcpack":
$newPack = new ZippedResourcePack($packPath);
break;
}
$this->resourcePacks[] = $newPack;
$index = strtolower($newPack->getPackId());
$this->uuidList[$index] = $newPack;
if($newPack instanceof ResourcePack){
$this->resourcePacks[] = $newPack;
$index = strtolower($newPack->getPackId());
$this->uuidList[$index] = $newPack;
$keyPath = Path::join($this->path, $pack . ".key");
if(file_exists($keyPath)){
try{
$this->encryptionKeys[$index] = ErrorToExceptionHandler::trapAndRemoveFalse(
fn() => file_get_contents($keyPath)
);
}catch(\ErrorException $e){
throw new ResourcePackException("Could not read encryption key file: " . $e->getMessage(), 0, $e);
}
$keyPath = Path::join($this->path, $pack . ".key");
if(file_exists($keyPath)){
try{
$key = Filesystem::fileGetContents($keyPath);
}catch(\RuntimeException $e){
throw new ResourcePackException("Could not read encryption key file: " . $e->getMessage(), 0, $e);
}
}else{
throw new ResourcePackException("Format not recognized");
$key = rtrim($key, "\r\n");
if(strlen($key) !== 32){
throw new ResourcePackException("Invalid encryption key length, must be exactly 32 bytes");
}
$this->encryptionKeys[$index] = $key;
}
}catch(ResourcePackException $e){
$logger->critical("Could not load resource pack \"$pack\": " . $e->getMessage());
@ -137,6 +121,25 @@ class ResourcePackManager{
$logger->debug("Successfully loaded " . count($this->resourcePacks) . " resource packs");
}
private function loadPackFromPath(string $packPath) : ResourcePack{
if(!file_exists($packPath)){
throw new ResourcePackException("File or directory not found");
}
if(is_dir($packPath)){
throw new ResourcePackException("Directory resource packs are unsupported");
}
//Detect the type of resource pack.
$info = new \SplFileInfo($packPath);
switch($info->getExtension()){
case "zip":
case "mcpack":
return new ZippedResourcePack($packPath);
}
throw new ResourcePackException("Format not recognized");
}
/**
* Returns the directory which resource packs are loaded from.
*/
@ -159,6 +162,29 @@ class ResourcePackManager{
return $this->resourcePacks;
}
/**
* Sets the resource packs to use. Packs earliest in the list will appear at the top of the stack (maximum
* priority), and later ones will appear below (lower priority), in the same manner as the Bedrock resource packs
* screen in-game.
*
* @param ResourcePack[] $resourceStack
* @phpstan-param list<ResourcePack> $resourceStack
*/
public function setResourceStack(array $resourceStack) : void{
$uuidList = [];
$resourcePacks = [];
foreach($resourceStack as $pack){
$uuid = strtolower($pack->getPackId());
if(isset($uuidList[$uuid])){
throw new \InvalidArgumentException("Cannot load two resource pack with the same UUID ($uuid)");
}
$uuidList[$uuid] = $pack;
$resourcePacks[] = $pack;
}
$this->resourcePacks = $resourcePacks;
$this->uuidList = $uuidList;
}
/**
* Returns the resource pack matching the specified UUID string, or null if the ID was not recognized.
*/
@ -180,4 +206,23 @@ class ResourcePackManager{
public function getPackEncryptionKey(string $id) : ?string{
return $this->encryptionKeys[strtolower($id)] ?? null;
}
/**
* Sets the encryption key to use for decrypting the specified resource pack. The pack will **NOT** be decrypted by
* PocketMine-MP; the key is simply passed to the client to allow it to decrypt the pack after downloading it.
*/
public function setPackEncryptionKey(string $id, ?string $key) : void{
$id = strtolower($id);
if($key === null){
//allow deprovisioning keys for resource packs that have been removed
unset($this->encryptionKeys[$id]);
}elseif(isset($this->uuidList[$id])){
if(strlen($key) !== 32){
throw new \InvalidArgumentException("Encryption key must be exactly 32 bytes long");
}
$this->encryptionKeys[$id] = $key;
}else{
throw new \InvalidArgumentException("Unknown pack ID $id");
}
}
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\timings;
use pocketmine\Server;
use function round;
use function spl_object_id;
@ -54,8 +55,8 @@ final class TimingsRecord{
public static function tick(bool $measure = true) : void{
if($measure){
foreach(self::$records as $record){
if($record->curTickTotal > 50000000){
$record->violations += (int) round($record->curTickTotal / 50000000);
if($record->curTickTotal > Server::TARGET_NANOSECONDS_PER_TICK){
$record->violations += (int) round($record->curTickTotal / Server::TARGET_NANOSECONDS_PER_TICK);
}
$record->curTickTotal = 0;
$record->curCount = 0;

View File

@ -33,7 +33,6 @@ use function count;
use function date;
use function explode;
use function file_exists;
use function file_get_contents;
use function get_debug_type;
use function implode;
use function is_array;
@ -162,10 +161,7 @@ class Config{
$this->config = $default;
$this->save();
}else{
$content = file_get_contents($this->file);
if($content === false){
throw new \RuntimeException("Unable to load config file");
}
$content = Filesystem::fileGetContents($this->file);
switch($this->type){
case Config::PROPERTIES:
$config = self::parseProperties($content);

View File

@ -30,6 +30,7 @@ use function dirname;
use function fclose;
use function fflush;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function flock;
use function fopen;
@ -47,9 +48,9 @@ use function rmdir;
use function rtrim;
use function scandir;
use function str_replace;
use function str_starts_with;
use function stream_get_contents;
use function strlen;
use function strpos;
use function uksort;
use function unlink;
use const DIRECTORY_SEPARATOR;
@ -171,7 +172,7 @@ final class Filesystem{
//this should probably never have integer keys, but it's safer than making PHPStan ignore it
foreach(Utils::stringifyKeys(self::$cleanedPaths) as $cleanPath => $replacement){
$cleanPath = rtrim(str_replace([DIRECTORY_SEPARATOR, "phar://"], ["/", ""], $cleanPath), "/");
if(strpos($result, $cleanPath) === 0){
if(str_starts_with($result, $cleanPath)){
$result = ltrim(str_replace($cleanPath, $replacement, $result), "/");
}
}
@ -296,4 +297,21 @@ final class Filesystem{
@unlink($temporaryFileName);
}
}
/**
* Wrapper around file_get_contents() which throws an exception instead of generating E_* errors.
*
* @phpstan-param resource|null $context
* @phpstan-param 0|positive-int $offset
* @phpstan-param 0|positive-int|null $length
*
* @throws \RuntimeException
*/
public static function fileGetContents(string $fileName, bool $useIncludePath = false, $context = null, int $offset = 0, ?int $length = null) : string{
try{
return ErrorToExceptionHandler::trapAndRemoveFalse(fn() => file_get_contents($fileName, $useIncludePath, $context, $offset, $length));
}catch(\ErrorException $e){
throw new \RuntimeException("Failed to read file $fileName: " . $e->getMessage(), 0, $e);
}
}
}

View File

@ -38,8 +38,8 @@ use function posix_kill;
use function preg_match;
use function proc_close;
use function proc_open;
use function str_starts_with;
use function stream_get_contents;
use function strpos;
use function trim;
final class Process{
@ -99,9 +99,9 @@ final class Process{
if($mappings === false) throw new AssumptionFailedError("/proc/self/maps should always be accessible");
foreach($mappings as $line){
if(preg_match("#([a-z0-9]+)\\-([a-z0-9]+) [rwxp\\-]{4} [a-z0-9]+ [^\\[]*\\[([a-zA-z0-9]+)\\]#", trim($line), $matches) > 0){
if(strpos($matches[3], "heap") === 0){
if(str_starts_with($matches[3], "heap")){
$heap += (int) hexdec($matches[2]) - (int) hexdec($matches[1]);
}elseif(strpos($matches[3], "stack") === 0){
}elseif(str_starts_with($matches[3], "stack")){
$stack += (int) hexdec($matches[2]) - (int) hexdec($matches[1]);
}
}

View File

@ -37,8 +37,9 @@ use function json_decode;
use function parse_ini_file;
use function preg_match;
use function readlink;
use function str_contains;
use function str_replace;
use function strpos;
use function str_starts_with;
use function substr;
use function timezone_abbreviations_list;
use function timezone_name_from_abbr;
@ -61,7 +62,7 @@ abstract class Timezone{
* This is here so that people don't come to us complaining and fill up the issue tracker when they put
* an incorrect timezone abbreviation in php.ini apparently.
*/
if(strpos($timezone, "/") === false){
if(!str_contains($timezone, "/")){
$default_timezone = timezone_name_from_abbr($timezone);
if($default_timezone !== false){
ini_set("date.timezone", $default_timezone);
@ -165,7 +166,7 @@ abstract class Timezone{
return self::parseOffset($offset);
case Utils::OS_MACOS:
$filename = @readlink('/etc/localtime');
if($filename !== false && strpos($filename, '/usr/share/zoneinfo/') === 0){
if($filename !== false && str_starts_with($filename, '/usr/share/zoneinfo/')){
$timezone = substr($filename, 20);
return trim($timezone);
}
@ -183,11 +184,11 @@ abstract class Timezone{
*/
private static function parseOffset($offset){
//Make signed offsets unsigned for date_parse
if(strpos($offset, '-') !== false){
if(str_starts_with($offset, '-')){
$negative_offset = true;
$offset = str_replace('-', '', $offset);
}else{
if(strpos($offset, '+') !== false){
if(str_starts_with($offset, '+')){
$negative_offset = false;
$offset = str_replace('+', '', $offset);
}else{

View File

@ -79,11 +79,12 @@ use function preg_match_all;
use function preg_replace;
use function shell_exec;
use function spl_object_id;
use function str_ends_with;
use function str_pad;
use function str_split;
use function str_starts_with;
use function stripos;
use function strlen;
use function strpos;
use function substr;
use function sys_get_temp_dir;
use function trim;
@ -119,7 +120,7 @@ final class Utils{
*/
public static function getNiceClosureName(\Closure $closure) : string{
$func = new \ReflectionFunction($closure);
if(substr($func->getName(), -strlen('{closure}')) !== '{closure}'){
if(!str_ends_with($func->getName(), '{closure}')){
//closure wraps a named function, can be done with reflection or fromCallable()
//isClosure() is useless here because it just tells us if $func is reflecting a Closure object
@ -273,7 +274,7 @@ final class Utils{
if(self::$os === null || $recalculate){
$uname = php_uname("s");
if(stripos($uname, "Darwin") !== false){
if(strpos(php_uname("m"), "iP") === 0){
if(str_starts_with(php_uname("m"), "iP")){
self::$os = self::OS_IOS;
}else{
self::$os = self::OS_MACOS;

View File

@ -50,7 +50,9 @@ use pocketmine\event\world\ChunkLoadEvent;
use pocketmine\event\world\ChunkPopulateEvent;
use pocketmine\event\world\ChunkUnloadEvent;
use pocketmine\event\world\SpawnChangeEvent;
use pocketmine\event\world\WorldParticleEvent;
use pocketmine\event\world\WorldSaveEvent;
use pocketmine\event\world\WorldSoundEvent;
use pocketmine\item\Item;
use pocketmine\item\ItemUseResult;
use pocketmine\item\LegacyStringToItemParser;
@ -666,9 +668,19 @@ class World implements ChunkManager{
* @param Player[]|null $players
*/
public function addSound(Vector3 $pos, Sound $sound, ?array $players = null) : void{
$pk = $sound->encode($pos);
$players ??= $this->getViewersForPosition($pos);
$ev = new WorldSoundEvent($this, $sound, $pos, $players);
$ev->call();
if($ev->isCancelled()){
return;
}
$pk = $ev->getSound()->encode($pos);
$players = $ev->getRecipients();
if(count($pk) > 0){
if($players === null){
if($players === $this->getViewersForPosition($pos)){
foreach($pk as $e){
$this->broadcastPacketToViewers($pos, $e);
}
@ -682,14 +694,24 @@ class World implements ChunkManager{
* @param Player[]|null $players
*/
public function addParticle(Vector3 $pos, Particle $particle, ?array $players = null) : void{
$pk = $particle->encode($pos);
$players ??= $this->getViewersForPosition($pos);
$ev = new WorldParticleEvent($this, $particle, $pos, $players);
$ev->call();
if($ev->isCancelled()){
return;
}
$pk = $ev->getParticle()->encode($pos);
$players = $ev->getRecipients();
if(count($pk) > 0){
if($players === null){
if($players === $this->getViewersForPosition($pos)){
foreach($pk as $e){
$this->broadcastPacketToViewers($pos, $e);
}
}else{
$this->server->broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
$this->server->broadcastPackets($this->filterViewersForPosition($pos, $ev->getRecipients()), $pk);
}
}
}
@ -1130,6 +1152,8 @@ class World implements ChunkManager{
/** @var bool[] $chunkTickList chunkhash => dummy */
$chunkTickList = [];
$chunkTickableCache = [];
$centerChunks = [];
$selector = new ChunkSelector();
@ -1149,7 +1173,7 @@ class World implements ChunkManager{
$centerChunkZ
) as $hash){
World::getXZ($hash, $chunkX, $chunkZ);
if(!isset($chunkTickList[$hash]) && isset($this->chunks[$hash]) && $this->isChunkTickable($chunkX, $chunkZ)){
if(!isset($chunkTickList[$hash]) && isset($this->chunks[$hash]) && $this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){
$chunkTickList[$hash] = true;
}
}
@ -1164,14 +1188,29 @@ class World implements ChunkManager{
}
}
private function isChunkTickable(int $chunkX, int $chunkZ) : bool{
/**
* @param bool[] &$cache
*
* @phpstan-param array<int, bool> $cache
* @phpstan-param-out array<int, bool> $cache
*/
private function isChunkTickable(int $chunkX, int $chunkZ, array &$cache) : bool{
for($cx = -1; $cx <= 1; ++$cx){
for($cz = -1; $cz <= 1; ++$cz){
$chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
if(isset($cache[$chunkHash])){
if(!$cache[$chunkHash]){
return false;
}
continue;
}
if($this->isChunkLocked($chunkX + $cx, $chunkZ + $cz)){
$cache[$chunkHash] = false;
return false;
}
$adjacentChunk = $this->getChunk($chunkX + $cx, $chunkZ + $cz);
if($adjacentChunk === null || !$adjacentChunk->isPopulated()){
$cache[$chunkHash] = false;
return false;
}
$lightPopulatedState = $adjacentChunk->isLightPopulated();
@ -1179,8 +1218,11 @@ class World implements ChunkManager{
if($lightPopulatedState === false){
$this->orderLightPopulation($chunkX + $cx, $chunkZ + $cz);
}
$cache[$chunkHash] = false;
return false;
}
$cache[$chunkHash] = true;
}
}
@ -1749,10 +1791,7 @@ class World implements ChunkManager{
if($update){
$this->updateAllLight($x, $y, $z);
$this->tryAddToNeighbourUpdateQueue($pos);
foreach($pos->sides() as $side){
$this->tryAddToNeighbourUpdateQueue($side);
}
$this->notifyNeighbourBlockUpdate($pos);
}
$this->timings->setBlock->stopTiming();
@ -2797,6 +2836,36 @@ class World implements ChunkManager{
}
/**
* Requests a safe spawn position near the given position, or near the world's spawn position if not provided.
* Terrain near the position will be loaded or generated as needed.
*
* @return Promise Resolved to a Position object, or rejected if the world is unloaded.
* @phpstan-return Promise<Position>
*/
public function requestSafeSpawn(?Vector3 $spawn = null) : Promise{
$resolver = new PromiseResolver();
$spawn ??= $this->getSpawnLocation();
/*
* TODO: this relies on the assumption that getSafeSpawn() will only alter the Y coordinate of the provided
* position, which is currently OK, but might be a problem in the future.
*/
$this->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
function() use ($spawn, $resolver) : void{
$spawn = $this->getSafeSpawn($spawn);
$resolver->resolve($spawn);
},
function() use ($resolver) : void{
$resolver->reject();
}
);
return $resolver->getPromise();
}
/**
* Returns a safe spawn position near the given position, or near the world's spawn position if not provided.
* This function will throw an exception if the terrain is not already generated in advance.
*
* @throws WorldException if the terrain is not generated
*/
public function getSafeSpawn(?Vector3 $spawn = null) : Position{

View File

@ -55,12 +55,14 @@ use function strval;
use function trim;
class WorldManager{
public const TICKS_PER_AUTOSAVE = 300 * Server::TARGET_TICKS_PER_SECOND;
/** @var World[] */
private array $worlds = [];
private ?World $defaultWorld = null;
private bool $autoSave = true;
private int $autoSaveTicks = 6000;
private int $autoSaveTicks = self::TICKS_PER_AUTOSAVE;
private int $autoSaveTicker = 0;
public function __construct(
@ -348,8 +350,8 @@ class WorldManager{
$world->doTick($currentTick);
$tickMs = (microtime(true) - $worldTime) * 1000;
$world->tickRateTime = $tickMs;
if($tickMs >= 50){
$world->getLogger()->debug(sprintf("Tick took too long: %gms (%g ticks)", $tickMs, round($tickMs / 50, 2)));
if($tickMs >= Server::TARGET_SECONDS_PER_TICK * 1000){
$world->getLogger()->debug(sprintf("Tick took too long: %gms (%g ticks)", $tickMs, round($tickMs / (Server::TARGET_SECONDS_PER_TICK * 1000), 2)));
}
}

View File

@ -39,7 +39,6 @@ use pocketmine\world\generator\GeneratorManager;
use pocketmine\world\World;
use pocketmine\world\WorldCreationOptions;
use Symfony\Component\Filesystem\Path;
use function file_get_contents;
use function file_put_contents;
use function strlen;
use function substr;
@ -111,9 +110,10 @@ class BedrockWorldData extends BaseNbtWorldData{
}
protected function load() : CompoundTag{
$rawLevelData = @file_get_contents($this->dataPath);
if($rawLevelData === false){
throw new CorruptedWorldException("Failed to read level.dat (permission denied or doesn't exist)");
try{
$rawLevelData = Filesystem::fileGetContents($this->dataPath);
}catch(\RuntimeException $e){
throw new CorruptedWorldException($e->getMessage(), 0, $e);
}
if(strlen($rawLevelData) <= 8){
throw new CorruptedWorldException("Truncated level.dat");

View File

@ -37,7 +37,6 @@ use pocketmine\world\World;
use pocketmine\world\WorldCreationOptions;
use Symfony\Component\Filesystem\Path;
use function ceil;
use function file_get_contents;
use function file_put_contents;
use function microtime;
use function zlib_decode;
@ -75,9 +74,10 @@ class JavaWorldData extends BaseNbtWorldData{
}
protected function load() : CompoundTag{
$rawLevelData = @file_get_contents($this->dataPath);
if($rawLevelData === false){
throw new CorruptedWorldException("Failed to read level.dat (permission denied or doesn't exist)");
try{
$rawLevelData = Filesystem::fileGetContents($this->dataPath);
}catch(\RuntimeException $e){
throw new CorruptedWorldException($e->getMessage(), 0, $e);
}
$nbt = new BigEndianNbtSerializer();
$decompressed = @zlib_decode($rawLevelData);

View File

@ -617,7 +617,7 @@ parameters:
-
message: "#^Cannot call method getLanguage\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
count: 1
count: 2
path: ../../../src/network/mcpe/NetworkSession.php
-