mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-10 03:34:06 +00:00
Compare commits
105 Commits
Author | SHA1 | Date | |
---|---|---|---|
4fc5b9772a | |||
5d4880b0a7 | |||
2b1a0e1e72 | |||
cd022f1592 | |||
4ae3fd7734 | |||
b2249f93c0 | |||
303344783a | |||
75e0844ff5 | |||
18fabf5466 | |||
2751c59979 | |||
d99ffbd66c | |||
a34f3261cb | |||
8ce0022de6 | |||
fb6491ddeb | |||
3b961d0e5f | |||
a60fc4cc28 | |||
b747899fdd | |||
57b6451e16 | |||
8cf025a2df | |||
8480ee82ea | |||
a6c1b7bf9c | |||
c267137fde | |||
461bc94236 | |||
4fed08bcd4 | |||
e990c5a0a5 | |||
c616d9bb7c | |||
81051441ba | |||
3ecae0db19 | |||
c5bbb2bcbc | |||
24a2889758 | |||
60b26a7ea8 | |||
22b52f03d1 | |||
df76c02e7a | |||
d343187e58 | |||
c5ad127854 | |||
0f6dc9082a | |||
2b6dcbc2e2 | |||
763c8ebfe3 | |||
c572e9bb6a | |||
89521f166d | |||
49d3a42120 | |||
c523595e85 | |||
7c7e4f2093 | |||
88c1014f03 | |||
e32180ce93 | |||
e105578be0 | |||
a9d98bdf73 | |||
c601352777 | |||
77c71e22b2 | |||
1c13ba5656 | |||
f970be0e4d | |||
11a3f9f1b9 | |||
09771849ae | |||
57a310230a | |||
130c55d9f1 | |||
2712befa82 | |||
a4e250a3e6 | |||
23b97d8e2d | |||
1fb5043eb1 | |||
b0b1b29de4 | |||
1c3b641e37 | |||
f3063e797f | |||
8dcc88712c | |||
04191ec44a | |||
62ea7c93a9 | |||
cf06b5b8cf | |||
a8ec51daac | |||
6a7b77fee2 | |||
da42c8d020 | |||
b902f9ded0 | |||
9bb8a8f761 | |||
63b14a083c | |||
627a7c951a | |||
bb2685ca65 | |||
d38709a7ae | |||
b559a65346 | |||
b92a2ded8a | |||
22f25dfbdb | |||
6bf840c72e | |||
745be19a56 | |||
e05bee5ffb | |||
087ba0cc1d | |||
d8d994351b | |||
0029efa370 | |||
df13e967fd | |||
097c260dbb | |||
a219b727f2 | |||
710c162604 | |||
409c8c1703 | |||
376926c700 | |||
c3fabe833e | |||
3e09ff5350 | |||
7255065106 | |||
a7f10d8ccf | |||
76f1add3b3 | |||
fcc9e62c65 | |||
42613618a5 | |||
1bbeb62457 | |||
64893426fa | |||
3d50aafcc4 | |||
50fed41642 | |||
3f971a0c65 | |||
a27b29897c | |||
a90132a30e | |||
dfbd857771 |
@ -3,7 +3,7 @@
|
||||
<b>A highly customisable, open source server software for Minecraft: Bedrock Edition written in PHP</b>
|
||||
</p>
|
||||
|
||||
[](https://travis-ci.org/pmmp/PocketMine-MP)
|
||||
[](https://travis-ci.com/pmmp/PocketMine-MP)
|
||||
|
||||
## Getting started
|
||||
- [Documentation](http://pmmp.readthedocs.org/)
|
||||
|
Submodule build/php updated: 0aa88d2765...cec63c3093
@ -49,7 +49,7 @@ require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
function preg_quote_array(array $strings, string $delim = null) : array{
|
||||
function preg_quote_array(array $strings, string $delim) : array{
|
||||
return array_map(function(string $str) use ($delim) : string{ return preg_quote($str, $delim); }, $strings);
|
||||
}
|
||||
|
||||
|
@ -50,4 +50,9 @@ Plugin developers should **only** update their required API to this version if y
|
||||
- Fixed absorption hearts not being consumed.
|
||||
|
||||
# 3.12.5
|
||||
- Fixed broken attack cooldowns.
|
||||
- Fixed broken attack cooldowns.
|
||||
|
||||
# 3.12.6
|
||||
- Fixed entities not getting movement updates after teleports.
|
||||
- Fixed slow flight in spectator mode when starting from the ground and after teleportation.
|
||||
- Errors communicating with the crash archive on automatic crash submission are now logged.
|
||||
|
@ -109,3 +109,23 @@ AsyncTask thread-local storage has been improved, making it simpler and easier t
|
||||
- `Utils::recursiveUnlink()`
|
||||
- `Terminal::write()`
|
||||
- `Terminal::writeLine()`
|
||||
|
||||
# 3.13.1
|
||||
- Fixed issues with `server.lock` not being unlocked on some platforms. Now, the server explicitly releases it before exiting.
|
||||
- `/timings` now sends a usage message when using an unknown subcommand. Previously, it would just give no output.
|
||||
- `/whitelist` now sends a usage message when using an unknown subcommand. Previously, it would just give no output.
|
||||
- The output from `/timings` is now broadcasted on the `pocketmine.broadcast.admin` broadcast channel for auditability, similarly to other operator commands.
|
||||
- Fixed `ShapedRecipe` deprecation warning on PHP 7.4.
|
||||
- Fixed some potential crashes with Bedrock worlds when chunk data is corrupted or missing.
|
||||
- Fixed a bug in region handling that caused region loaders to overestimate the amount of space used in the file. This resulted in an up to 4 MB growth of the file size every time the region was reloaded after writing a chunk.
|
||||
- Region handlers now try to reuse free space in region files before putting the chunk at the end of the file. Previously, space was only reused if the new version of the chunk was <= the size of the old. This fixes endless growth of region files.
|
||||
- Regions now never directly overwrite old copies of chunks when saving; instead they try to find an alternative location (preferring unused space within the file first). This avoids chunk corruption on power failure (the old copy of the chunk won't be damaged, so a rollback might occur instead), and as happy side effect, causes oversized regions to gradually shrink towards their most packed state over time, saving disk space.
|
||||
- Regions now have a hard size cap at 64 GB. This is because the header pointers will overflow beyond 64 GB (besides, a normal region shouldn't be this big anyway).
|
||||
- Fixed a crash that could occur when reading a too-short region header.
|
||||
- `VerifyLoginTask` now only copies JWTs to verify instead of the entire login packet. This reduces the amount of data copied between threads, improving performance.
|
||||
- Added a fast-fail check to `VerifyLoginTask` by checking the JWT header's `x5u` against the expected public key.
|
||||
- `Skin->validate()` now throws `InvalidSkinException` instead of `\InvalidArgumentException`.
|
||||
- A debug message is now logged when a player is kicked for having an invalid skin, giving a brief line of detail why.
|
||||
- Fixed players not being kicked for having an invalid `resourcePatch`.
|
||||
- Fixed block meta value of cake being preserved when using pick-block.
|
||||
- Fixed explosions not fully destroying multi-block objects like beds and doors.
|
||||
|
25
changelogs/3.14.md
Normal file
25
changelogs/3.14.md
Normal file
@ -0,0 +1,25 @@
|
||||
**For Minecraft: Bedrock Edition 1.16.0**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the protocol and compatible with any previous 3.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.
|
||||
|
||||
# 3.14.0
|
||||
- Added support for Minecraft: Bedrock Edition 1.16.0.
|
||||
- Removed compatibility with 1.14.60.
|
||||
|
||||
## Known issues (please don't open issues for these)
|
||||
- Walls don't connect to each other
|
||||
- Pumpkin and melon stems may not connect to their corresponding pumpkin/melon
|
||||
- New blocks, items & mobs aren't implemented
|
||||
- Nether doesn't exist
|
||||
|
||||
# 3.14.1
|
||||
- All skins are now trusted, bypassing the client-side trusted skin setting. Note that this means that NSFW skin filtering will **not** apply.
|
||||
- Fixed projectile motion being altered by ladders.
|
||||
- Fixed client-sided crashes when pressing E repeatedly very quickly on a high-latency connection.
|
||||
- `/plugins`, `/whitelist list`, `/banlist` and `/list` now show output in alphabetical order.
|
||||
- Some `pocketmine\event` APIs which accept arrays now have more robust type checking, fixing type errors caused by plugin input occurring in core code.
|
||||
- `Attribute::getAttributeByName()` is now aware of the `minecraft:lava_movement` attribute.
|
@ -38,10 +38,10 @@
|
||||
"ocramius/package-versions": "^1.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^0.12.25",
|
||||
"phpstan/phpstan": "0.12.29",
|
||||
"phpstan/phpstan-phpunit": "^0.12.6",
|
||||
"phpstan/phpstan-strict-rules": "^0.12.2",
|
||||
"phpunit/phpunit": "^8.5"
|
||||
"phpunit/phpunit": "^9.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
842
composer.lock
generated
842
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
||||
includes:
|
||||
- tests/phpstan/configs/actual-problems.neon
|
||||
- tests/phpstan/configs/check-explicit-mixed-baseline.neon
|
||||
- tests/phpstan/configs/com-dotnet-magic.neon
|
||||
- tests/phpstan/configs/custom-leveldb.neon
|
||||
- tests/phpstan/configs/gc-hacks.neon
|
||||
@ -16,8 +17,10 @@ includes:
|
||||
|
||||
parameters:
|
||||
level: 8
|
||||
autoload_files:
|
||||
checkExplicitMixed: true
|
||||
bootstrapFiles:
|
||||
- tests/phpstan/bootstrap.php
|
||||
scanFiles:
|
||||
- src/pocketmine/PocketMine.php
|
||||
- build/make-release.php
|
||||
- build/server-phar.php
|
||||
@ -32,6 +35,7 @@ parameters:
|
||||
stubFiles:
|
||||
- tests/phpstan/stubs/pthreads.stub
|
||||
- tests/phpstan/stubs/chunkutils.stub
|
||||
- tests/phpstan/stubs/leveldb.stub
|
||||
reportUnmatchedIgnoredErrors: false #no other way to silence platform-specific non-warnings
|
||||
staticReflectionClassNamePatterns:
|
||||
- "#^COM$#"
|
||||
|
@ -50,6 +50,7 @@ use function is_object;
|
||||
use function is_resource;
|
||||
use function is_string;
|
||||
use function json_encode;
|
||||
use function mb_strtoupper;
|
||||
use function min;
|
||||
use function mkdir;
|
||||
use function preg_match;
|
||||
@ -58,7 +59,6 @@ use function round;
|
||||
use function spl_object_hash;
|
||||
use function sprintf;
|
||||
use function strlen;
|
||||
use function strtoupper;
|
||||
use function substr;
|
||||
use const JSON_PRETTY_PRINT;
|
||||
use const JSON_UNESCAPED_SLASHES;
|
||||
@ -127,7 +127,7 @@ class MemoryManager{
|
||||
if($m <= 0){
|
||||
$defaultMemory = 0;
|
||||
}else{
|
||||
switch(strtoupper($matches[2])){
|
||||
switch(mb_strtoupper($matches[2])){
|
||||
case "K":
|
||||
$defaultMemory = $m / 1024;
|
||||
break;
|
||||
|
@ -33,6 +33,7 @@ use pocketmine\entity\Effect;
|
||||
use pocketmine\entity\EffectInstance;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\entity\Human;
|
||||
use pocketmine\entity\InvalidSkinException;
|
||||
use pocketmine\entity\object\ItemEntity;
|
||||
use pocketmine\entity\projectile\Arrow;
|
||||
use pocketmine\entity\Skin;
|
||||
@ -112,6 +113,7 @@ use pocketmine\network\mcpe\protocol\BlockPickRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\BookEditPacket;
|
||||
use pocketmine\network\mcpe\protocol\ChunkRadiusUpdatedPacket;
|
||||
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
|
||||
use pocketmine\network\mcpe\protocol\ContainerOpenPacket;
|
||||
use pocketmine\network\mcpe\protocol\DataPacket;
|
||||
use pocketmine\network\mcpe\protocol\DisconnectPacket;
|
||||
use pocketmine\network\mcpe\protocol\InteractPacket;
|
||||
@ -147,6 +149,7 @@ use pocketmine\network\mcpe\protocol\types\CommandParameter;
|
||||
use pocketmine\network\mcpe\protocol\types\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\DimensionIds;
|
||||
use pocketmine\network\mcpe\protocol\types\GameMode;
|
||||
use pocketmine\network\mcpe\protocol\types\NetworkInventoryAction;
|
||||
use pocketmine\network\mcpe\protocol\types\PersonaPieceTintColor;
|
||||
use pocketmine\network\mcpe\protocol\types\PersonaSkinPiece;
|
||||
use pocketmine\network\mcpe\protocol\types\PlayerPermissions;
|
||||
@ -154,6 +157,8 @@ use pocketmine\network\mcpe\protocol\types\SkinAdapterSingleton;
|
||||
use pocketmine\network\mcpe\protocol\types\SkinAnimation;
|
||||
use pocketmine\network\mcpe\protocol\types\SkinData;
|
||||
use pocketmine\network\mcpe\protocol\types\SkinImage;
|
||||
use pocketmine\network\mcpe\protocol\types\SpawnSettings;
|
||||
use pocketmine\network\mcpe\protocol\types\WindowTypes;
|
||||
use pocketmine\network\mcpe\protocol\UpdateAttributesPacket;
|
||||
use pocketmine\network\mcpe\protocol\UpdateBlockPacket;
|
||||
use pocketmine\network\mcpe\VerifyLoginTask;
|
||||
@ -171,7 +176,9 @@ use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\utils\UUID;
|
||||
use function abs;
|
||||
use function array_key_exists;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function base64_decode;
|
||||
use function ceil;
|
||||
@ -222,6 +229,14 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
|
||||
private const RESOURCE_PACK_CHUNK_SIZE = 128 * 1024; //128KB
|
||||
|
||||
//TODO: HACK!
|
||||
//these IDs are used for 1.16 to restore 1.14ish crafting & inventory behaviour; since they don't seem to have any
|
||||
//effect on the behaviour of inventory transactions I don't currently plan to integrate these into the main system.
|
||||
private const RESERVED_WINDOW_ID_RANGE_START = ContainerIds::LAST - 10;
|
||||
private const RESERVED_WINDOW_ID_RANGE_END = ContainerIds::LAST;
|
||||
public const HARDCODED_CRAFTING_GRID_WINDOW_ID = self::RESERVED_WINDOW_ID_RANGE_START + 1;
|
||||
public const HARDCODED_INVENTORY_WINDOW_ID = self::RESERVED_WINDOW_ID_RANGE_START + 2;
|
||||
|
||||
/**
|
||||
* Validates the given username.
|
||||
*/
|
||||
@ -301,6 +316,15 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
/** @var CraftingTransaction|null */
|
||||
protected $craftingTransaction = null;
|
||||
|
||||
/**
|
||||
* TODO: HACK! This tracks GUIs for inventories that the server considers "always open" so that the client can't
|
||||
* open them twice. (1.16 hack)
|
||||
* @var true[]
|
||||
* @phpstan-var array<int, true>
|
||||
* @internal
|
||||
*/
|
||||
public $openHardcodedWindows = [];
|
||||
|
||||
/** @var int */
|
||||
protected $messageCounter = 2;
|
||||
/** @var bool */
|
||||
@ -742,7 +766,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
}
|
||||
$data->aliases = new CommandEnum();
|
||||
$data->aliases->enumName = ucfirst($command->getName()) . "Aliases";
|
||||
$data->aliases->enumValues = $aliases;
|
||||
$data->aliases->enumValues = array_values($aliases);
|
||||
}
|
||||
|
||||
$pk->commandData[$command->getName()] = $data;
|
||||
@ -1227,11 +1251,12 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
}
|
||||
$this->spawnPosition = new Position($pos->x, $pos->y, $pos->z, $level);
|
||||
$pk = new SetSpawnPositionPacket();
|
||||
$pk->x = $this->spawnPosition->getFloorX();
|
||||
$pk->y = $this->spawnPosition->getFloorY();
|
||||
$pk->z = $this->spawnPosition->getFloorZ();
|
||||
$pk->x = $pk->x2 = $this->spawnPosition->getFloorX();
|
||||
$pk->y = $pk->y2 = $this->spawnPosition->getFloorY();
|
||||
$pk->z = $pk->z2 = $this->spawnPosition->getFloorZ();
|
||||
$pk->dimension = DimensionIds::OVERWORLD;
|
||||
$pk->spawnType = SetSpawnPositionPacket::TYPE_PLAYER_SPAWN;
|
||||
$pk->spawnForced = false;
|
||||
|
||||
$this->dataPacket($pk);
|
||||
}
|
||||
|
||||
@ -1956,9 +1981,11 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
true
|
||||
);
|
||||
|
||||
$skin = SkinAdapterSingleton::get()->fromSkinData($skinData);
|
||||
|
||||
if(!$skin->isValid()){
|
||||
try{
|
||||
$skin = SkinAdapterSingleton::get()->fromSkinData($skinData);
|
||||
$skin->validate();
|
||||
}catch(InvalidSkinException $e){
|
||||
$this->server->getLogger()->debug("$this->username: Invalid skin: " . $e->getMessage());
|
||||
$this->close("", "disconnectionScreen.invalidSkin");
|
||||
|
||||
return true;
|
||||
@ -2197,7 +2224,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
$pk->pitch = $this->pitch;
|
||||
$pk->yaw = $this->yaw;
|
||||
$pk->seed = -1;
|
||||
$pk->dimension = DimensionIds::OVERWORLD; //TODO: implement this properly
|
||||
$pk->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly
|
||||
$pk->worldGamemode = Player::getClientFriendlyGamemode($this->server->getGamemode());
|
||||
$pk->difficulty = $this->level->getDifficulty();
|
||||
$pk->spawnX = $spawnPosition->getFloorX();
|
||||
@ -2364,7 +2391,20 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
|
||||
/** @var InventoryAction[] $actions */
|
||||
$actions = [];
|
||||
$isCraftingPart = false;
|
||||
$isFinalCraftingPart = false;
|
||||
foreach($packet->actions as $networkInventoryAction){
|
||||
if(
|
||||
$networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_TODO and (
|
||||
$networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_RESULT or
|
||||
$networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_USE_INGREDIENT
|
||||
)
|
||||
){
|
||||
$isCraftingPart = true;
|
||||
if($networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_RESULT){
|
||||
$isFinalCraftingPart = true;
|
||||
}
|
||||
}
|
||||
try{
|
||||
$action = $networkInventoryAction->createInventoryAction($this);
|
||||
if($action !== null){
|
||||
@ -2377,7 +2417,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
}
|
||||
}
|
||||
|
||||
if($packet->isCraftingPart){
|
||||
if($isCraftingPart){
|
||||
if($this->craftingTransaction === null){
|
||||
$this->craftingTransaction = new CraftingTransaction($this, $actions);
|
||||
}else{
|
||||
@ -2386,7 +2426,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
}
|
||||
}
|
||||
|
||||
if($packet->isFinalCraftingPart){
|
||||
if($isFinalCraftingPart){
|
||||
//we get the actions for this in several packets, so we need to wait until we have all the pieces before
|
||||
//trying to execute it
|
||||
|
||||
@ -2764,8 +2804,23 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
case InteractPacket::ACTION_LEAVE_VEHICLE:
|
||||
case InteractPacket::ACTION_MOUSEOVER:
|
||||
break; //TODO: handle these
|
||||
case InteractPacket::ACTION_OPEN_INVENTORY:
|
||||
if($target === $this && !array_key_exists($windowId = self::HARDCODED_INVENTORY_WINDOW_ID, $this->openHardcodedWindows)){
|
||||
//TODO: HACK! this restores 1.14ish behaviour, but this should be able to be listened to and
|
||||
//controlled by plugins. However, the player is always a subscriber to their own inventory so it
|
||||
//doesn't integrate well with the regular container system right now.
|
||||
$this->openHardcodedWindows[$windowId] = true;
|
||||
$pk = new ContainerOpenPacket();
|
||||
$pk->windowId = $windowId;
|
||||
$pk->type = WindowTypes::INVENTORY;
|
||||
$pk->x = $pk->y = $pk->z = 0;
|
||||
$pk->entityUniqueId = $this->getId();
|
||||
$this->sendDataPacket($pk);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
$this->server->getLogger()->debug("Unhandled/unknown interaction type " . $packet->action . "received from " . $this->getName());
|
||||
$this->server->getLogger()->debug("Unhandled/unknown interaction type " . $packet->action . " received from " . $this->getName());
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -2977,18 +3032,23 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
}
|
||||
|
||||
public function handleContainerClose(ContainerClosePacket $packet) : bool{
|
||||
if(!$this->spawned or $packet->windowId === 0){
|
||||
if(!$this->spawned){
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->doCloseInventory();
|
||||
|
||||
if(array_key_exists($packet->windowId, $this->openHardcodedWindows)){
|
||||
unset($this->openHardcodedWindows[$packet->windowId]);
|
||||
$pk = new ContainerClosePacket();
|
||||
$pk->windowId = $packet->windowId;
|
||||
$this->sendDataPacket($pk);
|
||||
return true;
|
||||
}
|
||||
if(isset($this->windowIndex[$packet->windowId])){
|
||||
(new InventoryCloseEvent($this->windowIndex[$packet->windowId], $this))->call();
|
||||
$this->removeWindow($this->windowIndex[$packet->windowId]);
|
||||
return true;
|
||||
}elseif($packet->windowId === 255){
|
||||
//Closed a fake window
|
||||
//removeWindow handles sending the appropriate
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -3514,14 +3574,15 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
* Sends a Form to the player, or queue to send it if a form is already open.
|
||||
*/
|
||||
public function sendForm(Form $form) : void{
|
||||
$formData = json_encode($form);
|
||||
if($formData === false){
|
||||
throw new \InvalidArgumentException("Failed to encode form JSON: " . json_last_error_msg());
|
||||
}
|
||||
$id = $this->formIdCounter++;
|
||||
$pk = new ModalFormRequestPacket();
|
||||
$pk->formId = $id;
|
||||
$pk->formData = json_encode($form);
|
||||
if($pk->formData === false){
|
||||
throw new \InvalidArgumentException("Failed to encode form JSON: " . json_last_error_msg());
|
||||
}
|
||||
if($this->dataPacket($pk)){
|
||||
$pk->formData = $formData;
|
||||
if($this->dataPacket($pk) !== false){
|
||||
$this->forms[$id] = $form;
|
||||
}
|
||||
}
|
||||
@ -3862,6 +3923,9 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
|
||||
$this->resetFallDistance();
|
||||
$this->nextChunkOrderRun = 0;
|
||||
if($this->spawnChunkLoadCount !== -1){
|
||||
$this->spawnChunkLoadCount = 0;
|
||||
}
|
||||
$this->stopSleep();
|
||||
|
||||
//TODO: workaround for player last pos not getting updated
|
||||
@ -3957,7 +4021,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
if($forceId === null){
|
||||
$cnt = $this->windowCnt;
|
||||
do{
|
||||
$cnt = max(ContainerIds::FIRST, ($cnt + 1) % ContainerIds::LAST);
|
||||
$cnt = max(ContainerIds::FIRST, ($cnt + 1) % self::RESERVED_WINDOW_ID_RANGE_START);
|
||||
if($cnt === $this->windowCnt){ //wraparound, no free slots
|
||||
throw new \InvalidStateException("No free window IDs found");
|
||||
}
|
||||
@ -3965,7 +4029,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
$this->windowCnt = $cnt;
|
||||
}else{
|
||||
$cnt = $forceId;
|
||||
if(isset($this->windowIndex[$cnt])){
|
||||
if(isset($this->windowIndex[$cnt]) or ($cnt >= self::RESERVED_WINDOW_ID_RANGE_START && $cnt <= self::RESERVED_WINDOW_ID_RANGE_END)){
|
||||
throw new \InvalidArgumentException("Requested force ID $forceId already in use");
|
||||
}
|
||||
}
|
||||
|
@ -289,6 +289,14 @@ namespace pocketmine {
|
||||
|
||||
echo Terminal::$FORMAT_RESET . PHP_EOL;
|
||||
|
||||
if(!flock(\pocketmine\LOCK_FILE, LOCK_UN)){
|
||||
critical_error("Failed to release the server.lock file.");
|
||||
}
|
||||
|
||||
if(!fclose(\pocketmine\LOCK_FILE)){
|
||||
critical_error("Could not close server.lock resource.");
|
||||
}
|
||||
|
||||
exit($exitCode);
|
||||
}
|
||||
|
||||
|
@ -693,29 +693,32 @@ class Server{
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getPlayerDataPath(string $username) : string{
|
||||
return $this->getDataPath() . '/players/' . strtolower($username) . '.dat';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the server has stored any saved data for this player.
|
||||
*/
|
||||
public function hasOfflinePlayerData(string $name) : bool{
|
||||
$name = strtolower($name);
|
||||
return file_exists($this->getDataPath() . "players/$name.dat");
|
||||
return file_exists($this->getPlayerDataPath($name));
|
||||
}
|
||||
|
||||
public function getOfflinePlayerData(string $name) : CompoundTag{
|
||||
$name = strtolower($name);
|
||||
$path = $this->getDataPath() . "players/";
|
||||
$path = $this->getPlayerDataPath($name);
|
||||
if($this->shouldSavePlayerData()){
|
||||
if(file_exists($path . "$name.dat")){
|
||||
if(file_exists($path)){
|
||||
try{
|
||||
$nbt = new BigEndianNBTStream();
|
||||
$compound = $nbt->readCompressed(file_get_contents($path . "$name.dat"));
|
||||
$compound = $nbt->readCompressed(file_get_contents($path));
|
||||
if(!($compound instanceof CompoundTag)){
|
||||
throw new \RuntimeException("Invalid data found in \"$name.dat\", expected " . CompoundTag::class . ", got " . (is_object($compound) ? get_class($compound) : gettype($compound)));
|
||||
}
|
||||
|
||||
return $compound;
|
||||
}catch(\Throwable $e){ //zlib decode error / corrupt data
|
||||
rename($path . "$name.dat", $path . "$name.dat.bak");
|
||||
rename($path, $path . '.bak');
|
||||
$this->logger->notice($this->getLanguage()->translateString("pocketmine.data.playerCorrupted", [$name]));
|
||||
}
|
||||
}else{
|
||||
@ -776,7 +779,7 @@ class Server{
|
||||
if(!$ev->isCancelled()){
|
||||
$nbt = new BigEndianNBTStream();
|
||||
try{
|
||||
file_put_contents($this->getDataPath() . "players/" . strtolower($name) . ".dat", $nbt->writeCompressed($ev->getSaveData()));
|
||||
file_put_contents($this->getPlayerDataPath($name), $nbt->writeCompressed($ev->getSaveData()));
|
||||
}catch(\Throwable $e){
|
||||
$this->logger->critical($this->getLanguage()->translateString("pocketmine.data.saveError", [$name, $e->getMessage()]));
|
||||
$this->logger->logException($e);
|
||||
|
@ -76,11 +76,9 @@ abstract class Thread extends \Thread{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $options TODO: pthreads bug
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function start(?int $options = PTHREADS_INHERIT_ALL){
|
||||
public function start(int $options = PTHREADS_INHERIT_ALL){
|
||||
ThreadManager::getInstance()->add($this);
|
||||
|
||||
if($this->getClassLoader() === null){
|
||||
|
@ -33,6 +33,6 @@ if(defined('pocketmine\_VERSION_INFO_INCLUDED')){
|
||||
const _VERSION_INFO_INCLUDED = true;
|
||||
|
||||
const NAME = "PocketMine-MP";
|
||||
const BASE_VERSION = "3.13.0";
|
||||
const BASE_VERSION = "3.14.2";
|
||||
const IS_DEVELOPMENT_BUILD = false;
|
||||
const BUILD_NUMBER = 0;
|
||||
|
@ -76,11 +76,9 @@ abstract class Worker extends \Worker{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $options TODO: pthreads bug
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function start(?int $options = PTHREADS_INHERIT_ALL){
|
||||
public function start(int $options = PTHREADS_INHERIT_ALL){
|
||||
ThreadManager::getInstance()->add($this);
|
||||
|
||||
if($this->getClassLoader() === null){
|
||||
|
@ -36,7 +36,7 @@ use pocketmine\math\RayTraceResult;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\metadata\Metadatable;
|
||||
use pocketmine\metadata\MetadataValue;
|
||||
use pocketmine\network\mcpe\protocol\types\RuntimeBlockMapping;
|
||||
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
|
||||
use pocketmine\Player;
|
||||
use pocketmine\plugin\Plugin;
|
||||
use function array_merge;
|
||||
|
@ -25,7 +25,7 @@ namespace pocketmine\block;
|
||||
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\level\Position;
|
||||
use pocketmine\network\mcpe\protocol\types\RuntimeBlockMapping;
|
||||
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
|
||||
use function min;
|
||||
|
||||
/**
|
||||
|
@ -109,6 +109,10 @@ class Cake extends Transparent implements FoodSource{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getVariantBitmask() : int{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Block
|
||||
*/
|
||||
|
@ -25,7 +25,10 @@ namespace pocketmine\block;
|
||||
|
||||
use pocketmine\inventory\CraftingGrid;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\protocol\ContainerOpenPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\WindowTypes;
|
||||
use pocketmine\Player;
|
||||
use function array_key_exists;
|
||||
|
||||
class CraftingTable extends Solid{
|
||||
|
||||
@ -50,6 +53,19 @@ class CraftingTable extends Solid{
|
||||
public function onActivate(Item $item, Player $player = null) : bool{
|
||||
if($player instanceof Player){
|
||||
$player->setCraftingGrid(new CraftingGrid($player, CraftingGrid::SIZE_BIG));
|
||||
|
||||
if(!array_key_exists($windowId = Player::HARDCODED_CRAFTING_GRID_WINDOW_ID, $player->openHardcodedWindows)){
|
||||
//TODO: HACK! crafting grid doesn't fit very well into the current PM container system, so this hack allows
|
||||
//it to carry on working approximately the same way as it did in 1.14
|
||||
$pk = new ContainerOpenPacket();
|
||||
$pk->windowId = $windowId;
|
||||
$pk->type = WindowTypes::WORKBENCH;
|
||||
$pk->x = $this->getFloorX();
|
||||
$pk->y = $this->getFloorY();
|
||||
$pk->z = $this->getFloorZ();
|
||||
$player->sendDataPacket($pk);
|
||||
$player->openHardcodedWindows[$windowId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\AxisAlignedBB;
|
||||
use pocketmine\math\Vector3;
|
||||
@ -58,7 +59,7 @@ class Ladder extends Transparent{
|
||||
}
|
||||
|
||||
public function onEntityCollide(Entity $entity) : void{
|
||||
if($entity->asVector3()->floor()->distanceSquared($this) < 1){ //entity coordinates must be inside block
|
||||
if($entity instanceof Living and $entity->asVector3()->floor()->distanceSquared($this) < 1){ //entity coordinates must be inside block
|
||||
$entity->resetFallDistance();
|
||||
$entity->onGround = true;
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ class Rail extends BaseRail{
|
||||
}
|
||||
|
||||
protected function getPossibleConnectionDirectionsOneConstraint(int $constraint) : array{
|
||||
/** @var int[] $horizontal */
|
||||
static $horizontal = [
|
||||
Vector3::SIDE_NORTH,
|
||||
Vector3::SIDE_SOUTH,
|
||||
|
@ -30,7 +30,9 @@ use pocketmine\permission\BanEntry;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function sort;
|
||||
use function strtolower;
|
||||
use const SORT_STRING;
|
||||
|
||||
class BanListCommand extends VanillaCommand{
|
||||
|
||||
@ -62,10 +64,11 @@ class BanListCommand extends VanillaCommand{
|
||||
$args[0] = "players";
|
||||
}
|
||||
|
||||
$list = $list->getEntries();
|
||||
$message = implode(", ", array_map(function(BanEntry $entry) : string{
|
||||
$list = array_map(function(BanEntry $entry) : string{
|
||||
return $entry->getName();
|
||||
}, $list));
|
||||
}, $list->getEntries());
|
||||
sort($list, SORT_STRING);
|
||||
$message = implode(", ", $list);
|
||||
|
||||
if($args[0] === "ips"){
|
||||
$sender->sendMessage(new TranslationContainer("commands.banlist.ips", [count($list)]));
|
||||
|
@ -30,6 +30,8 @@ use function array_filter;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function sort;
|
||||
use const SORT_STRING;
|
||||
|
||||
class ListCommand extends VanillaCommand{
|
||||
|
||||
@ -52,6 +54,7 @@ class ListCommand extends VanillaCommand{
|
||||
}, array_filter($sender->getServer()->getOnlinePlayers(), function(Player $player) use ($sender) : bool{
|
||||
return $player->isOnline() and (!($sender instanceof Player) or $sender->canSee($player));
|
||||
}));
|
||||
sort($playerNames, SORT_STRING);
|
||||
|
||||
$sender->sendMessage(new TranslationContainer("commands.players.list", [count($playerNames), $sender->getServer()->getMaxPlayers()]));
|
||||
$sender->sendMessage(implode(", ", $playerNames));
|
||||
|
@ -30,6 +30,8 @@ use pocketmine\utils\TextFormat;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function sort;
|
||||
use const SORT_STRING;
|
||||
|
||||
class PluginsCommand extends VanillaCommand{
|
||||
|
||||
@ -51,6 +53,7 @@ class PluginsCommand extends VanillaCommand{
|
||||
$list = array_map(function(Plugin $plugin) : string{
|
||||
return ($plugin->isEnabled() ? TextFormat::GREEN : TextFormat::RED) . $plugin->getDescription()->getFullName();
|
||||
}, $sender->getServer()->getPluginManager()->getPlugins());
|
||||
sort($list, SORT_STRING);
|
||||
|
||||
$sender->sendMessage(new TranslationContainer("pocketmine.command.plugins.success", [count($list), implode(TextFormat::WHITE . ", ", $list)]));
|
||||
return true;
|
||||
|
@ -27,11 +27,11 @@ use pocketmine\command\Command;
|
||||
use pocketmine\command\CommandSender;
|
||||
use pocketmine\command\utils\InvalidCommandSyntaxException;
|
||||
use pocketmine\lang\TranslationContainer;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\level\Location;
|
||||
use pocketmine\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
use function array_shift;
|
||||
use function count;
|
||||
use function round;
|
||||
|
||||
@ -47,84 +47,83 @@ class TeleportCommand extends VanillaCommand{
|
||||
$this->setPermission("pocketmine.command.teleport");
|
||||
}
|
||||
|
||||
private function findPlayer(CommandSender $sender, string $playerName) : ?Player{
|
||||
$subject = $sender->getServer()->getPlayer($playerName);
|
||||
if($subject === null){
|
||||
$sender->sendMessage(TextFormat::RED . "Can't find player " . $playerName);
|
||||
return null;
|
||||
}
|
||||
return $subject;
|
||||
}
|
||||
|
||||
public function execute(CommandSender $sender, string $commandLabel, array $args){
|
||||
if(!$this->testPermission($sender)){
|
||||
return true;
|
||||
}
|
||||
|
||||
$args = array_values(array_filter($args, function(string $arg) : bool{
|
||||
return $arg !== "";
|
||||
}));
|
||||
if(count($args) < 1 or count($args) > 6){
|
||||
throw new InvalidCommandSyntaxException();
|
||||
}
|
||||
|
||||
$target = null;
|
||||
$origin = $sender;
|
||||
|
||||
if(count($args) === 1 or count($args) === 3){
|
||||
if($sender instanceof Player){
|
||||
$target = $sender;
|
||||
}else{
|
||||
$sender->sendMessage(TextFormat::RED . "Please provide a player!");
|
||||
|
||||
return true;
|
||||
}
|
||||
if(count($args) === 1){
|
||||
$target = $sender->getServer()->getPlayer($args[0]);
|
||||
if($target === null){
|
||||
$sender->sendMessage(TextFormat::RED . "Can't find player " . $args[0]);
|
||||
|
||||
switch(count($args)){
|
||||
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;
|
||||
}
|
||||
}
|
||||
}else{
|
||||
$target = $sender->getServer()->getPlayer($args[0]);
|
||||
if($target === null){
|
||||
$sender->sendMessage(TextFormat::RED . "Can't find player " . $args[0]);
|
||||
|
||||
return true;
|
||||
}
|
||||
if(count($args) === 2){
|
||||
$origin = $target;
|
||||
$target = $sender->getServer()->getPlayer($args[1]);
|
||||
if($target === null){
|
||||
$sender->sendMessage(TextFormat::RED . "Can't find player " . $args[1]);
|
||||
|
||||
$subject = $sender;
|
||||
$targetArgs = $args;
|
||||
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);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidCommandSyntaxException();
|
||||
}
|
||||
|
||||
if(count($args) < 3){
|
||||
$origin->teleport($target);
|
||||
Command::broadcastCommandMessage($sender, new TranslationContainer("commands.tp.success", [$origin->getName(), $target->getName()]));
|
||||
switch(count($targetArgs)){
|
||||
case 1:
|
||||
$targetPlayer = $this->findPlayer($sender, $targetArgs[0]);
|
||||
if($targetPlayer === null){
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}elseif($target->isValid()){
|
||||
if(count($args) === 4 or count($args) === 6){
|
||||
$pos = 1;
|
||||
}else{
|
||||
$pos = 0;
|
||||
}
|
||||
$subject->teleport($targetPlayer->getLocation());
|
||||
Command::broadcastCommandMessage($sender, new TranslationContainer("commands.tp.success", [$subject->getName(), $targetPlayer->getName()]));
|
||||
|
||||
$x = $this->getRelativeDouble($target->x, $sender, $args[$pos++]);
|
||||
$y = $this->getRelativeDouble($target->y, $sender, $args[$pos++], 0, 256);
|
||||
$z = $this->getRelativeDouble($target->z, $sender, $args[$pos++]);
|
||||
$yaw = $target->getYaw();
|
||||
$pitch = $target->getPitch();
|
||||
return true;
|
||||
case 3:
|
||||
case 5:
|
||||
$base = $subject->getLocation();
|
||||
if(count($targetArgs) === 5){
|
||||
$yaw = (float) $targetArgs[3];
|
||||
$pitch = (float) $targetArgs[4];
|
||||
}else{
|
||||
$yaw = $base->yaw;
|
||||
$pitch = $base->pitch;
|
||||
}
|
||||
|
||||
if(count($args) === 6 or (count($args) === 5 and $pos === 3)){
|
||||
$yaw = (float) $args[$pos++];
|
||||
$pitch = (float) $args[$pos++];
|
||||
}
|
||||
$x = $this->getRelativeDouble($base->x, $sender, $targetArgs[0]);
|
||||
$y = $this->getRelativeDouble($base->y, $sender, $targetArgs[1], 0, 256);
|
||||
$z = $this->getRelativeDouble($base->z, $sender, $targetArgs[2]);
|
||||
$targetLocation = new Location($x, $y, $z, $yaw, $pitch, $base->getLevelNonNull());
|
||||
|
||||
$target->teleport(new Vector3($x, $y, $z), $yaw, $pitch);
|
||||
Command::broadcastCommandMessage($sender, new TranslationContainer("commands.tp.success.coordinates", [$target->getName(), round($x, 2), round($y, 2), round($z, 2)]));
|
||||
|
||||
return true;
|
||||
$subject->teleport($targetLocation);
|
||||
Command::broadcastCommandMessage($sender, new TranslationContainer("commands.tp.success.coordinates", [
|
||||
$subject->getName(),
|
||||
round($targetLocation->x, 2),
|
||||
round($targetLocation->y, 2),
|
||||
round($targetLocation->z, 2)
|
||||
]));
|
||||
return true;
|
||||
default:
|
||||
throw new AssumptionFailedError("This branch should be unreachable (for now)");
|
||||
}
|
||||
|
||||
throw new InvalidCommandSyntaxException();
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\command\defaults;
|
||||
|
||||
use pocketmine\command\Command;
|
||||
use pocketmine\command\CommandSender;
|
||||
use pocketmine\command\utils\InvalidCommandSyntaxException;
|
||||
use pocketmine\lang\TranslationContainer;
|
||||
@ -72,12 +73,12 @@ class TimingsCommand extends VanillaCommand{
|
||||
|
||||
if($mode === "on"){
|
||||
TimingsHandler::setEnabled();
|
||||
$sender->sendMessage(new TranslationContainer("pocketmine.command.timings.enable"));
|
||||
Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.enable"));
|
||||
|
||||
return true;
|
||||
}elseif($mode === "off"){
|
||||
TimingsHandler::setEnabled(false);
|
||||
$sender->sendMessage(new TranslationContainer("pocketmine.command.timings.disable"));
|
||||
Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.disable"));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -91,7 +92,7 @@ class TimingsCommand extends VanillaCommand{
|
||||
|
||||
if($mode === "reset"){
|
||||
TimingsHandler::reload();
|
||||
$sender->sendMessage(new TranslationContainer("pocketmine.command.timings.reset"));
|
||||
Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.reset"));
|
||||
}elseif($mode === "merged" or $mode === "report" or $paste){
|
||||
$timings = "";
|
||||
if($paste){
|
||||
@ -150,6 +151,7 @@ class TimingsCommand extends VanillaCommand{
|
||||
}
|
||||
|
||||
public function onCompletion(Server $server){
|
||||
/** @var CommandSender $sender */
|
||||
$sender = $this->fetchLocal();
|
||||
if($sender instanceof Player and !$sender->isOnline()){ // TODO replace with a more generic API method for checking availability of CommandSender
|
||||
return;
|
||||
@ -159,18 +161,21 @@ class TimingsCommand extends VanillaCommand{
|
||||
$server->getLogger()->logException($result);
|
||||
return;
|
||||
}
|
||||
if(isset($result[0]) && is_array($response = json_decode($result[0], true)) && isset($response["id"])){
|
||||
$sender->sendMessage(new TranslationContainer("pocketmine.command.timings.timingsRead",
|
||||
$response = json_decode($result[0], true);
|
||||
if(is_array($response) && isset($response["id"])){
|
||||
Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.timingsRead",
|
||||
["https://" . $this->host . "/?id=" . $response["id"]]));
|
||||
}else{
|
||||
$sender->sendMessage(new TranslationContainer("pocketmine.command.timings.pasteError"));
|
||||
Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.pasteError"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}else{
|
||||
fclose($fileTimings);
|
||||
$sender->sendMessage(new TranslationContainer("pocketmine.command.timings.timingsWrite", [$timings]));
|
||||
Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.timingsWrite", [$timings]));
|
||||
}
|
||||
}else{
|
||||
throw new InvalidCommandSyntaxException();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -31,7 +31,9 @@ use pocketmine\Player;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function sort;
|
||||
use function strtolower;
|
||||
use const SORT_STRING;
|
||||
|
||||
class WhitelistCommand extends VanillaCommand{
|
||||
|
||||
@ -49,10 +51,6 @@ class WhitelistCommand extends VanillaCommand{
|
||||
return true;
|
||||
}
|
||||
|
||||
if(count($args) === 0 or count($args) > 2){
|
||||
throw new InvalidCommandSyntaxException();
|
||||
}
|
||||
|
||||
if(count($args) === 1){
|
||||
if($this->badPerm($sender, strtolower($args[0]))){
|
||||
return false;
|
||||
@ -75,6 +73,7 @@ class WhitelistCommand extends VanillaCommand{
|
||||
return true;
|
||||
case "list":
|
||||
$entries = $sender->getServer()->getWhitelisted()->getAll(true);
|
||||
sort($entries, SORT_STRING);
|
||||
$result = implode($entries, ", ");
|
||||
$count = count($entries);
|
||||
|
||||
@ -112,7 +111,7 @@ class WhitelistCommand extends VanillaCommand{
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
throw new InvalidCommandSyntaxException();
|
||||
}
|
||||
|
||||
private function badPerm(CommandSender $sender, string $subcommand) : bool{
|
||||
|
@ -45,6 +45,7 @@ class Attribute{
|
||||
public const FALL_DAMAGE = 13;
|
||||
public const HORSE_JUMP_STRENGTH = 14;
|
||||
public const ZOMBIE_SPAWN_REINFORCEMENTS = 15;
|
||||
public const LAVA_MOVEMENT = 16;
|
||||
|
||||
/** @var int */
|
||||
private $id;
|
||||
@ -84,6 +85,7 @@ class Attribute{
|
||||
self::addAttribute(self::FALL_DAMAGE, "minecraft:fall_damage", 0.0, 340282346638528859811704183484516925440.0, 1.0);
|
||||
self::addAttribute(self::HORSE_JUMP_STRENGTH, "minecraft:horse.jump_strength", 0.0, 2.0, 0.7);
|
||||
self::addAttribute(self::ZOMBIE_SPAWN_REINFORCEMENTS, "minecraft:zombie.spawn_reinforcements", 0.0, 1.0, 0.0);
|
||||
self::addAttribute(self::LAVA_MOVEMENT, "minecraft:lava_movement", 0.0, 340282346638528859811704183484516925440.0, 0.02);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,7 +31,7 @@ use pocketmine\event\player\PlayerExhaustEvent;
|
||||
use pocketmine\utils\Color;
|
||||
use function constant;
|
||||
use function defined;
|
||||
use function strtoupper;
|
||||
use function mb_strtoupper;
|
||||
|
||||
class Effect{
|
||||
public const SPEED = 1;
|
||||
@ -102,7 +102,7 @@ class Effect{
|
||||
}
|
||||
|
||||
public static function getEffectByName(string $name) : ?Effect{
|
||||
$const = self::class . "::" . strtoupper($name);
|
||||
$const = self::class . "::" . mb_strtoupper($name);
|
||||
if(defined($const)){
|
||||
return self::getEffect(constant($const));
|
||||
}
|
||||
|
@ -311,12 +311,15 @@ abstract class Entity extends Location implements Metadatable, EntityIds{
|
||||
public const DATA_FLAG_ROARING = 83;
|
||||
public const DATA_FLAG_DELAYED_ATTACKING = 84;
|
||||
public const DATA_FLAG_AVOIDING_MOBS = 85;
|
||||
public const DATA_FLAG_FACING_TARGET_TO_RANGE_ATTACK = 86;
|
||||
public const DATA_FLAG_HIDDEN_WHEN_INVISIBLE = 87; //??????????????????
|
||||
public const DATA_FLAG_IS_IN_UI = 88;
|
||||
public const DATA_FLAG_STALKING = 89;
|
||||
public const DATA_FLAG_EMOTING = 90;
|
||||
public const DATA_FLAG_CELEBRATING = 91;
|
||||
public const DATA_FLAG_AVOIDING_BLOCK = 86;
|
||||
public const DATA_FLAG_FACING_TARGET_TO_RANGE_ATTACK = 87;
|
||||
public const DATA_FLAG_HIDDEN_WHEN_INVISIBLE = 88; //??????????????????
|
||||
public const DATA_FLAG_IS_IN_UI = 89;
|
||||
public const DATA_FLAG_STALKING = 90;
|
||||
public const DATA_FLAG_EMOTING = 91;
|
||||
public const DATA_FLAG_CELEBRATING = 92;
|
||||
public const DATA_FLAG_ADMIRING = 93;
|
||||
public const DATA_FLAG_CELEBRATING_SPECIAL = 94;
|
||||
|
||||
public const DATA_PLAYER_FLAG_SLEEP = 1;
|
||||
public const DATA_PLAYER_FLAG_DEAD = 2; //TODO: CHECK
|
||||
|
28
src/pocketmine/entity/InvalidSkinException.php
Normal file
28
src/pocketmine/entity/InvalidSkinException.php
Normal 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\entity;
|
||||
|
||||
final class InvalidSkinException extends \InvalidArgumentException{
|
||||
|
||||
}
|
@ -62,24 +62,24 @@ class Skin{
|
||||
try{
|
||||
$this->validate();
|
||||
return true;
|
||||
}catch(\InvalidArgumentException $e){
|
||||
}catch(InvalidSkinException $e){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws InvalidSkinException
|
||||
*/
|
||||
public function validate() : void{
|
||||
if($this->skinId === ""){
|
||||
throw new \InvalidArgumentException("Skin ID must not be empty");
|
||||
throw new InvalidSkinException("Skin ID must not be empty");
|
||||
}
|
||||
$len = strlen($this->skinData);
|
||||
if(!in_array($len, self::ACCEPTED_SKIN_SIZES, true)){
|
||||
throw new \InvalidArgumentException("Invalid skin data size $len bytes (allowed sizes: " . implode(", ", self::ACCEPTED_SKIN_SIZES) . ")");
|
||||
throw new InvalidSkinException("Invalid skin data size $len bytes (allowed sizes: " . implode(", ", self::ACCEPTED_SKIN_SIZES) . ")");
|
||||
}
|
||||
if($this->capeData !== "" and strlen($this->capeData) !== 8192){
|
||||
throw new \InvalidArgumentException("Invalid cape data size " . strlen($this->capeData) . " bytes (must be exactly 8192 bytes)");
|
||||
throw new InvalidSkinException("Invalid cape data size " . strlen($this->capeData) . " bytes (must be exactly 8192 bytes)");
|
||||
}
|
||||
//TODO: validate geometry
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ namespace pocketmine\event;
|
||||
|
||||
use function constant;
|
||||
use function defined;
|
||||
use function strtoupper;
|
||||
use function mb_strtoupper;
|
||||
|
||||
/**
|
||||
* List of event priorities
|
||||
@ -79,7 +79,7 @@ abstract class EventPriority{
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function fromString(string $name) : int{
|
||||
$name = strtoupper($name);
|
||||
$name = mb_strtoupper($name);
|
||||
$const = self::class . "::" . $name;
|
||||
if($name !== "ALL" and defined($const)){
|
||||
return constant($const);
|
||||
|
@ -26,6 +26,7 @@ namespace pocketmine\event\block;
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\Player;
|
||||
use pocketmine\utils\Utils;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
@ -79,6 +80,7 @@ class SignChangeEvent extends BlockEvent implements Cancellable{
|
||||
if(count($lines) !== 4){
|
||||
throw new \InvalidArgumentException("Array size must be 4!");
|
||||
}
|
||||
Utils::validateArrayValueType($lines, function(string $_) : void{});
|
||||
$this->lines = $lines;
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ use pocketmine\event\Cancellable;
|
||||
|
||||
/**
|
||||
* Called when an Entity, excluding players, changes a block directly
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityBlockChangeEvent extends EntityEvent implements Cancellable{
|
||||
/** @var Block */
|
||||
|
@ -26,6 +26,9 @@ namespace pocketmine\event\entity;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\event\Cancellable;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityCombustEvent extends EntityEvent implements Cancellable{
|
||||
/** @var int */
|
||||
protected $duration;
|
||||
|
@ -30,6 +30,7 @@ use function max;
|
||||
|
||||
/**
|
||||
* Called when an entity takes damage.
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityDamageEvent extends EntityEvent implements Cancellable{
|
||||
public const MODIFIER_ARMOR = 1;
|
||||
|
@ -25,7 +25,11 @@ namespace pocketmine\event\entity;
|
||||
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\utils\Utils;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<Living>
|
||||
*/
|
||||
class EntityDeathEvent extends EntityEvent{
|
||||
/** @var Item[] */
|
||||
private $drops = [];
|
||||
@ -59,6 +63,7 @@ class EntityDeathEvent extends EntityEvent{
|
||||
* @param Item[] $drops
|
||||
*/
|
||||
public function setDrops(array $drops) : void{
|
||||
Utils::validateArrayValueType($drops, function(Item $_) : void{});
|
||||
$this->drops = $drops;
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ use pocketmine\entity\Vehicle;
|
||||
|
||||
/**
|
||||
* Called when a entity is despawned
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityDespawnEvent extends EntityEvent{
|
||||
/** @var int */
|
||||
|
@ -27,6 +27,9 @@ use pocketmine\entity\EffectInstance;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\event\Cancellable;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityEffectEvent extends EntityEvent implements Cancellable{
|
||||
/** @var EffectInstance */
|
||||
private $effect;
|
||||
|
@ -29,12 +29,19 @@ namespace pocketmine\event\entity;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\event\Event;
|
||||
|
||||
/**
|
||||
* @phpstan-template TEntity of Entity
|
||||
*/
|
||||
abstract class EntityEvent extends Event{
|
||||
/** @var Entity */
|
||||
/**
|
||||
* @var Entity
|
||||
* @phpstan-var TEntity
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* @return Entity
|
||||
* @phpstan-return TEntity
|
||||
*/
|
||||
public function getEntity(){
|
||||
return $this->entity;
|
||||
|
@ -27,9 +27,11 @@ use pocketmine\block\Block;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\level\Position;
|
||||
use pocketmine\utils\Utils;
|
||||
|
||||
/**
|
||||
* Called when a entity explodes
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityExplodeEvent extends EntityEvent implements Cancellable{
|
||||
/** @var Position */
|
||||
@ -66,6 +68,7 @@ class EntityExplodeEvent extends EntityEvent implements Cancellable{
|
||||
* @param Block[] $blocks
|
||||
*/
|
||||
public function setBlockList(array $blocks) : void{
|
||||
Utils::validateArrayValueType($blocks, function(Block $_) : void{});
|
||||
$this->blocks = $blocks;
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ use pocketmine\item\Item;
|
||||
|
||||
/**
|
||||
* Called before a slot in an entity's inventory changes.
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityInventoryChangeEvent extends EntityEvent implements Cancellable{
|
||||
/** @var Item */
|
||||
|
@ -27,6 +27,9 @@ use pocketmine\entity\Entity;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\level\Level;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityLevelChangeEvent extends EntityEvent implements Cancellable{
|
||||
/** @var Level */
|
||||
private $originLevel;
|
||||
|
@ -27,6 +27,9 @@ use pocketmine\entity\Entity;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\math\Vector3;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityMotionEvent extends EntityEvent implements Cancellable{
|
||||
/** @var Vector3 */
|
||||
private $mot;
|
||||
|
@ -26,6 +26,9 @@ namespace pocketmine\event\entity;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\event\Cancellable;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityRegainHealthEvent extends EntityEvent implements Cancellable{
|
||||
public const CAUSE_REGEN = 0;
|
||||
public const CAUSE_EATING = 1;
|
||||
|
@ -30,6 +30,9 @@ use pocketmine\event\Cancellable;
|
||||
use pocketmine\item\Item;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<Living>
|
||||
*/
|
||||
class EntityShootBowEvent extends EntityEvent implements Cancellable{
|
||||
/** @var Item */
|
||||
private $bow;
|
||||
|
@ -33,6 +33,7 @@ use pocketmine\level\Position;
|
||||
|
||||
/**
|
||||
* Called when a entity is spawned
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntitySpawnEvent extends EntityEvent{
|
||||
/** @var int */
|
||||
|
@ -27,6 +27,9 @@ use pocketmine\entity\Entity;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\level\Position;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class EntityTeleportEvent extends EntityEvent implements Cancellable{
|
||||
/** @var Position */
|
||||
private $from;
|
||||
|
@ -28,6 +28,7 @@ use pocketmine\event\Cancellable;
|
||||
|
||||
/**
|
||||
* Called when a entity decides to explode
|
||||
* @phpstan-extends EntityEvent<Entity>
|
||||
*/
|
||||
class ExplosionPrimeEvent extends EntityEvent implements Cancellable{
|
||||
/** @var float */
|
||||
|
@ -26,6 +26,9 @@ namespace pocketmine\event\entity;
|
||||
use pocketmine\entity\object\ItemEntity;
|
||||
use pocketmine\event\Cancellable;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<ItemEntity>
|
||||
*/
|
||||
class ItemDespawnEvent extends EntityEvent implements Cancellable{
|
||||
|
||||
public function __construct(ItemEntity $item){
|
||||
|
@ -25,6 +25,9 @@ namespace pocketmine\event\entity;
|
||||
|
||||
use pocketmine\entity\object\ItemEntity;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<ItemEntity>
|
||||
*/
|
||||
class ItemSpawnEvent extends EntityEvent{
|
||||
|
||||
public function __construct(ItemEntity $item){
|
||||
|
@ -28,6 +28,7 @@ use pocketmine\math\RayTraceResult;
|
||||
|
||||
/**
|
||||
* @allowHandle
|
||||
* @phpstan-extends EntityEvent<Projectile>
|
||||
*/
|
||||
abstract class ProjectileHitEvent extends EntityEvent{
|
||||
/** @var RayTraceResult */
|
||||
|
@ -26,6 +26,9 @@ namespace pocketmine\event\entity;
|
||||
use pocketmine\entity\projectile\Projectile;
|
||||
use pocketmine\event\Cancellable;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<Projectile>
|
||||
*/
|
||||
class ProjectileLaunchEvent extends EntityEvent implements Cancellable{
|
||||
public function __construct(Projectile $entity){
|
||||
$this->entity = $entity;
|
||||
|
@ -28,6 +28,8 @@ use pocketmine\event\Cancellable;
|
||||
use pocketmine\permission\PermissionManager;
|
||||
use pocketmine\Player;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\utils\Utils;
|
||||
use function array_values;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
@ -97,6 +99,7 @@ class PlayerChatEvent extends PlayerEvent implements Cancellable{
|
||||
* @param CommandSender[] $recipients
|
||||
*/
|
||||
public function setRecipients(array $recipients) : void{
|
||||
Utils::validateArrayValueType($recipients, function(CommandSender $_) : void{});
|
||||
$this->recipients = $recipients;
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,9 @@ use pocketmine\entity\Human;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\event\entity\EntityEvent;
|
||||
|
||||
/**
|
||||
* @phpstan-extends EntityEvent<Human>
|
||||
*/
|
||||
class PlayerExhaustEvent extends EntityEvent implements Cancellable{
|
||||
public const CAUSE_ATTACK = 1;
|
||||
public const CAUSE_DAMAGE = 2;
|
||||
|
@ -29,6 +29,7 @@ use pocketmine\event\entity\EntityEvent;
|
||||
|
||||
/**
|
||||
* Called when a player gains or loses XP levels and/or progress.
|
||||
* @phpstan-extends EntityEvent<Human>
|
||||
*/
|
||||
class PlayerExperienceChangeEvent extends EntityEvent implements Cancellable{
|
||||
/** @var Human */
|
||||
|
@ -43,6 +43,9 @@ class PlayerRespawnEvent extends PlayerEvent{
|
||||
}
|
||||
|
||||
public function setRespawnPosition(Position $position) : void{
|
||||
if(!$position->isValid()){
|
||||
throw new \InvalidArgumentException("Spawn position must reference a valid and loaded World");
|
||||
}
|
||||
$this->position = $position;
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ use pocketmine\Player;
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\utils\Utils;
|
||||
use function chr;
|
||||
use function count;
|
||||
use function str_replace;
|
||||
@ -76,7 +77,7 @@ class QueryRegenerateEvent extends ServerEvent{
|
||||
|
||||
public function __construct(Server $server){
|
||||
$this->serverName = $server->getMotd();
|
||||
$this->listPlugins = $server->getProperty("settings.query-plugins", true);
|
||||
$this->listPlugins = (bool) $server->getProperty("settings.query-plugins", true);
|
||||
$this->plugins = $server->getPluginManager()->getPlugins();
|
||||
$this->players = [];
|
||||
foreach($server->getOnlinePlayers() as $player){
|
||||
@ -145,6 +146,7 @@ class QueryRegenerateEvent extends ServerEvent{
|
||||
* @param Plugin[] $plugins
|
||||
*/
|
||||
public function setPlugins(array $plugins) : void{
|
||||
Utils::validateArrayValueType($plugins, function(Plugin $_) : void{});
|
||||
$this->plugins = $plugins;
|
||||
$this->destroyCache();
|
||||
}
|
||||
@ -160,6 +162,7 @@ class QueryRegenerateEvent extends ServerEvent{
|
||||
* @param Player[] $players
|
||||
*/
|
||||
public function setPlayerList(array $players) : void{
|
||||
Utils::validateArrayValueType($players, function(Player $_) : void{});
|
||||
$this->players = $players;
|
||||
$this->destroyCache();
|
||||
}
|
||||
|
@ -28,7 +28,9 @@ use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\protocol\InventoryContentPacket;
|
||||
use pocketmine\network\mcpe\protocol\InventorySlotPacket;
|
||||
use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
||||
use pocketmine\Player;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
|
||||
class ArmorInventory extends BaseInventory{
|
||||
@ -109,7 +111,7 @@ class ArmorInventory extends BaseInventory{
|
||||
$pk2 = new InventorySlotPacket();
|
||||
$pk2->windowId = $player->getWindowId($this);
|
||||
$pk2->inventorySlot = $index;
|
||||
$pk2->item = $this->getItem($index);
|
||||
$pk2->item = ItemStackWrapper::legacy($this->getItem($index));
|
||||
$player->dataPacket($pk2);
|
||||
}else{
|
||||
$player->dataPacket($pk);
|
||||
@ -134,7 +136,7 @@ class ArmorInventory extends BaseInventory{
|
||||
if($player === $this->getHolder()){
|
||||
$pk2 = new InventoryContentPacket();
|
||||
$pk2->windowId = $player->getWindowId($this);
|
||||
$pk2->items = $this->getContents(true);
|
||||
$pk2->items = array_map([ItemStackWrapper::class, 'legacy'], $this->getContents(true));
|
||||
$player->dataPacket($pk2);
|
||||
}else{
|
||||
$player->dataPacket($pk);
|
||||
|
@ -31,7 +31,9 @@ use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\InventoryContentPacket;
|
||||
use pocketmine\network\mcpe\protocol\InventorySlotPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
||||
use pocketmine\Player;
|
||||
use function array_map;
|
||||
use function array_slice;
|
||||
use function count;
|
||||
use function max;
|
||||
@ -53,7 +55,7 @@ abstract class BaseInventory implements Inventory{
|
||||
protected $slots;
|
||||
/** @var Player[] */
|
||||
protected $viewers = [];
|
||||
/** @var InventoryEventProcessor */
|
||||
/** @var InventoryEventProcessor|null */
|
||||
protected $eventProcessor;
|
||||
|
||||
/**
|
||||
@ -433,7 +435,7 @@ abstract class BaseInventory implements Inventory{
|
||||
}
|
||||
|
||||
$pk = new InventoryContentPacket();
|
||||
$pk->items = $this->getContents(true);
|
||||
$pk->items = array_map([ItemStackWrapper::class, 'legacy'], $this->getContents(true));
|
||||
|
||||
foreach($target as $player){
|
||||
if(($id = $player->getWindowId($this)) === ContainerIds::NONE){
|
||||
@ -455,7 +457,7 @@ abstract class BaseInventory implements Inventory{
|
||||
|
||||
$pk = new InventorySlotPacket();
|
||||
$pk->inventorySlot = $index;
|
||||
$pk->item = $this->getItem($index);
|
||||
$pk->item = ItemStackWrapper::legacy($this->getItem($index));
|
||||
|
||||
foreach($target as $player){
|
||||
if(($id = $player->getWindowId($this)) === ContainerIds::NONE){
|
||||
|
@ -26,10 +26,12 @@ namespace pocketmine\inventory;
|
||||
use pocketmine\entity\Human;
|
||||
use pocketmine\event\player\PlayerItemHeldEvent;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\protocol\InventoryContentPacket;
|
||||
use pocketmine\network\mcpe\protocol\CreativeContentPacket;
|
||||
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
|
||||
use pocketmine\Player;
|
||||
use function array_map;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
|
||||
@ -196,16 +198,11 @@ class PlayerInventory extends BaseInventory{
|
||||
if(!($holder instanceof Player)){
|
||||
throw new \LogicException("Cannot send creative inventory contents to non-player inventory holder");
|
||||
}
|
||||
$pk = new InventoryContentPacket();
|
||||
$pk->windowId = ContainerIds::CREATIVE;
|
||||
|
||||
if(!$holder->isSpectator()){ //fill it for all gamemodes except spectator
|
||||
foreach(Item::getCreativeItems() as $i => $item){
|
||||
$pk->items[$i] = clone $item;
|
||||
}
|
||||
}
|
||||
|
||||
$holder->dataPacket($pk);
|
||||
$nextEntryId = 1;
|
||||
$holder->sendDataPacket(CreativeContentPacket::create(array_map(function(Item $item) use (&$nextEntryId) : CreativeContentEntry{
|
||||
return new CreativeContentEntry($nextEntryId++, clone $item);
|
||||
}, $holder->isSpectator() ? [] : Item::getCreativeItems()))); //fill it for all gamemodes except spectator
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -164,7 +164,7 @@ class ShapedRecipe implements CraftingRecipe{
|
||||
}
|
||||
|
||||
public function getIngredient(int $x, int $y) : Item{
|
||||
$exists = $this->ingredientList[$this->shape[$y]{$x}] ?? null;
|
||||
$exists = $this->ingredientList[$this->shape[$y][$x]] ?? null;
|
||||
return $exists !== null ? clone $exists : ItemFactory::get(Item::AIR, 0, 0);
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ use pocketmine\event\inventory\CraftItemEvent;
|
||||
use pocketmine\inventory\CraftingRecipe;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
|
||||
use pocketmine\network\mcpe\protocol\types\ContainerIds;
|
||||
use pocketmine\Player;
|
||||
use function array_pop;
|
||||
use function count;
|
||||
use function intdiv;
|
||||
@ -165,7 +165,7 @@ class CraftingTransaction extends InventoryTransaction{
|
||||
* transaction goes wrong.
|
||||
*/
|
||||
$pk = new ContainerClosePacket();
|
||||
$pk->windowId = ContainerIds::NONE;
|
||||
$pk->windowId = Player::HARDCODED_CRAFTING_GRID_WINDOW_ID;
|
||||
$this->source->dataPacket($pk);
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,6 @@ use pocketmine\block\Liquid;
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\level\sound\EndermanTeleportSound;
|
||||
use pocketmine\math\Vector3;
|
||||
use function assert;
|
||||
use function min;
|
||||
use function mt_rand;
|
||||
|
||||
|
@ -34,8 +34,8 @@ use function gettype;
|
||||
use function is_numeric;
|
||||
use function is_object;
|
||||
use function is_string;
|
||||
use function mb_strtoupper;
|
||||
use function str_replace;
|
||||
use function strtoupper;
|
||||
use function trim;
|
||||
|
||||
/**
|
||||
@ -375,8 +375,8 @@ class ItemFactory{
|
||||
|
||||
if(is_numeric($b[0])){
|
||||
$item = self::get((int) $b[0], $meta);
|
||||
}elseif(defined(ItemIds::class . "::" . strtoupper($b[0]))){
|
||||
$item = self::get(constant(ItemIds::class . "::" . strtoupper($b[0])), $meta);
|
||||
}elseif(defined(ItemIds::class . "::" . mb_strtoupper($b[0]))){
|
||||
$item = self::get(constant(ItemIds::class . "::" . mb_strtoupper($b[0])), $meta);
|
||||
}else{
|
||||
throw new \InvalidArgumentException("Unable to resolve \"" . $str . "\" to a valid item");
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace pocketmine\item\enchantment;
|
||||
use pocketmine\event\entity\EntityDamageEvent;
|
||||
use function constant;
|
||||
use function defined;
|
||||
use function strtoupper;
|
||||
use function mb_strtoupper;
|
||||
|
||||
/**
|
||||
* Manages enchantment type data.
|
||||
@ -69,6 +69,7 @@ class Enchantment{
|
||||
public const MULTISHOT = 33;
|
||||
public const PIERCING = 34;
|
||||
public const QUICK_CHARGE = 35;
|
||||
public const SOUL_SPEED = 36;
|
||||
|
||||
public const RARITY_COMMON = 10;
|
||||
public const RARITY_UNCOMMON = 5;
|
||||
@ -161,7 +162,7 @@ class Enchantment{
|
||||
}
|
||||
|
||||
public static function getEnchantmentByName(string $name) : ?Enchantment{
|
||||
$const = Enchantment::class . "::" . strtoupper($name);
|
||||
$const = Enchantment::class . "::" . mb_strtoupper($name);
|
||||
if(defined($const)){
|
||||
return self::getEnchantment(constant($const));
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ class BaseLang{
|
||||
$baseText = $this->parseTranslation(($onlyPrefix === null or strpos($str, $onlyPrefix) === 0) ? $baseText : $str, $onlyPrefix);
|
||||
|
||||
foreach($params as $i => $p){
|
||||
$baseText = str_replace("{%$i}", $this->parseTranslation((string) $p), $baseText, $onlyPrefix);
|
||||
$baseText = str_replace("{%$i}", $this->parseTranslation((string) $p), $baseText);
|
||||
}
|
||||
|
||||
return $baseText;
|
||||
|
@ -132,8 +132,11 @@ class Explosion{
|
||||
if($blockId !== 0){
|
||||
$blastForce -= (BlockFactory::$blastResistance[$blockId] / 5 + 0.3) * $this->stepLen;
|
||||
if($blastForce > 0){
|
||||
if(!isset($this->affectedBlocks[$index = Level::blockHash($vBlock->x, $vBlock->y, $vBlock->z)])){
|
||||
$this->affectedBlocks[$index] = BlockFactory::get($blockId, $this->subChunkHandler->currentSubChunk->getBlockData($vBlock->x & 0x0f, $vBlock->y & 0x0f, $vBlock->z & 0x0f), $vBlock);
|
||||
if(!isset($this->affectedBlocks[Level::blockHash($vBlock->x, $vBlock->y, $vBlock->z)])){
|
||||
$_block = BlockFactory::get($blockId, $this->subChunkHandler->currentSubChunk->getBlockData($vBlock->x & 0x0f, $vBlock->y & 0x0f, $vBlock->z & 0x0f), $vBlock);
|
||||
foreach($_block->getAffectedBlocks() as $_affectedBlock){
|
||||
$this->affectedBlocks[Level::blockHash($_affectedBlock->x, $_affectedBlock->y, $_affectedBlock->z)] = $_affectedBlock;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ use pocketmine\metadata\Metadatable;
|
||||
use pocketmine\metadata\MetadataValue;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
|
||||
use pocketmine\network\mcpe\protocol\AddActorPacket;
|
||||
use pocketmine\network\mcpe\protocol\BatchPacket;
|
||||
use pocketmine\network\mcpe\protocol\DataPacket;
|
||||
@ -79,7 +80,6 @@ use pocketmine\network\mcpe\protocol\LevelEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\SetDifficultyPacket;
|
||||
use pocketmine\network\mcpe\protocol\SetTimePacket;
|
||||
use pocketmine\network\mcpe\protocol\types\RuntimeBlockMapping;
|
||||
use pocketmine\network\mcpe\protocol\UpdateBlockPacket;
|
||||
use pocketmine\Player;
|
||||
use pocketmine\plugin\Plugin;
|
||||
@ -216,12 +216,18 @@ class Level implements ChunkManager, Metadatable{
|
||||
/** @var Vector3[][] */
|
||||
private $changedBlocks = [];
|
||||
|
||||
/** @var ReversePriorityQueue */
|
||||
/**
|
||||
* @var ReversePriorityQueue
|
||||
* @phpstan-var ReversePriorityQueue<int, Vector3>
|
||||
*/
|
||||
private $scheduledBlockUpdateQueue;
|
||||
/** @var int[] */
|
||||
private $scheduledBlockUpdateQueueIndex = [];
|
||||
|
||||
/** @var \SplQueue */
|
||||
/**
|
||||
* @var \SplQueue
|
||||
* @phpstan-var \SplQueue<int>
|
||||
*/
|
||||
private $neighbourBlockUpdateQueue;
|
||||
|
||||
/** @var Player[][] */
|
||||
|
@ -24,8 +24,8 @@ declare(strict_types=1);
|
||||
namespace pocketmine\level;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\utils\MainLogger;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\MainLogger;
|
||||
use function assert;
|
||||
|
||||
class Position extends Vector3{
|
||||
|
@ -109,6 +109,7 @@ class Chunk{
|
||||
* @param CompoundTag[] $entities
|
||||
* @param CompoundTag[] $tiles
|
||||
* @param int[] $heightMap
|
||||
* @phpstan-param list<int> $heightMap
|
||||
*/
|
||||
public function __construct(int $chunkX, int $chunkZ, array $subChunks = [], array $entities = [], array $tiles = [], string $biomeIds = "", array $heightMap = []){
|
||||
$this->x = $chunkX;
|
||||
|
@ -98,6 +98,7 @@ if(!extension_loaded('pocketmine_chunkutils')){
|
||||
* Converts pre-MCPE-1.0 biome color array to biome ID array.
|
||||
*
|
||||
* @param int[] $array of biome color values
|
||||
* @phpstan-param list<int> $array
|
||||
*/
|
||||
public static function convertBiomeColors(array $array) : string{
|
||||
$result = str_repeat("\x00", 256);
|
||||
|
@ -301,7 +301,8 @@ class LevelDB extends BaseLevelProvider{
|
||||
protected function readChunk(int $chunkX, int $chunkZ) : ?Chunk{
|
||||
$index = LevelDB::chunkIndex($chunkX, $chunkZ);
|
||||
|
||||
if(!$this->chunkExists($chunkX, $chunkZ)){
|
||||
$chunkVersionRaw = $this->db->get($index . self::TAG_VERSION);
|
||||
if($chunkVersionRaw === false){
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -316,7 +317,7 @@ class LevelDB extends BaseLevelProvider{
|
||||
/** @var bool $lightPopulated */
|
||||
$lightPopulated = true;
|
||||
|
||||
$chunkVersion = ord($this->db->get($index . self::TAG_VERSION));
|
||||
$chunkVersion = ord($chunkVersionRaw);
|
||||
$hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION;
|
||||
|
||||
$binaryStream = new BinaryStream();
|
||||
@ -368,7 +369,11 @@ class LevelDB extends BaseLevelProvider{
|
||||
}
|
||||
break;
|
||||
case 2: // < MCPE 1.0
|
||||
$binaryStream->setBuffer($this->db->get($index . self::TAG_LEGACY_TERRAIN));
|
||||
$legacyTerrain = $this->db->get($index . self::TAG_LEGACY_TERRAIN);
|
||||
if($legacyTerrain === false){
|
||||
throw new CorruptedChunkException("Expected to find a LEGACY_TERRAIN key for this chunk version, but none found");
|
||||
}
|
||||
$binaryStream->setBuffer($legacyTerrain);
|
||||
$fullIds = $binaryStream->get(32768);
|
||||
$fullData = $binaryStream->get(16384);
|
||||
$fullSkyLight = $binaryStream->get(16384);
|
||||
|
154
src/pocketmine/level/format/io/region/RegionGarbageMap.php
Normal file
154
src/pocketmine/level/format/io/region/RegionGarbageMap.php
Normal file
@ -0,0 +1,154 @@
|
||||
<?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\level\format\io\region;
|
||||
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use function end;
|
||||
use function ksort;
|
||||
use function time;
|
||||
use const SORT_NUMERIC;
|
||||
|
||||
final class RegionGarbageMap{
|
||||
|
||||
/** @var RegionLocationTableEntry[] */
|
||||
private $entries = [];
|
||||
/** @var bool */
|
||||
private $clean = false;
|
||||
|
||||
/**
|
||||
* @param RegionLocationTableEntry[] $entries
|
||||
*/
|
||||
public function __construct(array $entries){
|
||||
foreach($entries as $entry){
|
||||
$this->entries[$entry->getFirstSector()] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RegionLocationTableEntry[]|null[] $locationTable
|
||||
*/
|
||||
public static function buildFromLocationTable(array $locationTable) : self{
|
||||
/** @var RegionLocationTableEntry[] $usedMap */
|
||||
$usedMap = [];
|
||||
foreach($locationTable as $entry){
|
||||
if($entry === null){
|
||||
continue;
|
||||
}
|
||||
if(isset($usedMap[$entry->getFirstSector()])){
|
||||
throw new AssumptionFailedError("Overlapping entries detected");
|
||||
}
|
||||
$usedMap[$entry->getFirstSector()] = $entry;
|
||||
}
|
||||
|
||||
ksort($usedMap, SORT_NUMERIC);
|
||||
|
||||
/** @var RegionLocationTableEntry[] $garbageMap */
|
||||
$garbageMap = [];
|
||||
|
||||
/** @var RegionLocationTableEntry|null $prevEntry */
|
||||
$prevEntry = null;
|
||||
foreach($usedMap as $firstSector => $entry){
|
||||
$expectedStart = ($prevEntry !== null ? $prevEntry->getLastSector() + 1 : RegionLoader::FIRST_SECTOR);
|
||||
$actualStart = $entry->getFirstSector();
|
||||
if($expectedStart < $actualStart){
|
||||
//found a gap in the table
|
||||
$garbageMap[$expectedStart] = new RegionLocationTableEntry($expectedStart, $actualStart - $expectedStart, 0);
|
||||
}
|
||||
$prevEntry = $entry;
|
||||
}
|
||||
|
||||
return new self($garbageMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RegionLocationTableEntry[]
|
||||
* @phpstan-return array<int, RegionLocationTableEntry>
|
||||
*/
|
||||
public function getArray() : array{
|
||||
if(!$this->clean){
|
||||
ksort($this->entries, SORT_NUMERIC);
|
||||
|
||||
/** @var int|null $prevIndex */
|
||||
$prevIndex = null;
|
||||
foreach($this->entries as $k => $entry){
|
||||
if($prevIndex !== null and $this->entries[$prevIndex]->getLastSector() + 1 === $entry->getFirstSector()){
|
||||
//this SHOULD overwrite the previous index and not appear at the end
|
||||
$this->entries[$prevIndex] = new RegionLocationTableEntry(
|
||||
$this->entries[$prevIndex]->getFirstSector(),
|
||||
$this->entries[$prevIndex]->getSectorCount() + $entry->getSectorCount(),
|
||||
0
|
||||
);
|
||||
unset($this->entries[$k]);
|
||||
}else{
|
||||
$prevIndex = $k;
|
||||
}
|
||||
}
|
||||
$this->clean = true;
|
||||
}
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
public function add(RegionLocationTableEntry $entry) : void{
|
||||
if(isset($this->entries[$k = $entry->getFirstSector()])){
|
||||
throw new \InvalidArgumentException("Overlapping entry starting at " . $k);
|
||||
}
|
||||
$this->entries[$k] = $entry;
|
||||
$this->clean = false;
|
||||
}
|
||||
|
||||
public function remove(RegionLocationTableEntry $entry) : void{
|
||||
if(isset($this->entries[$k = $entry->getFirstSector()])){
|
||||
//removal doesn't affect ordering and shouldn't affect fragmentation
|
||||
unset($this->entries[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
public function end() : ?RegionLocationTableEntry{
|
||||
$array = $this->getArray();
|
||||
$end = end($array);
|
||||
return $end !== false ? $end : null;
|
||||
}
|
||||
|
||||
public function allocate(int $newSize) : ?RegionLocationTableEntry{
|
||||
foreach($this->getArray() as $start => $candidate){
|
||||
$candidateSize = $candidate->getSectorCount();
|
||||
if($candidateSize < $newSize){
|
||||
continue;
|
||||
}
|
||||
|
||||
$newLocation = new RegionLocationTableEntry($candidate->getFirstSector(), $newSize, time());
|
||||
$this->remove($candidate);
|
||||
|
||||
if($candidateSize > $newSize){ //we're not using the whole area, just take part of it
|
||||
$newGarbageStart = $candidate->getFirstSector() + $newSize;
|
||||
$newGarbageSize = $candidateSize - $newSize;
|
||||
$this->add(new RegionLocationTableEntry($newGarbageStart, $newGarbageSize, 0));
|
||||
}
|
||||
return $newLocation;
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ use pocketmine\level\format\io\exception\CorruptedChunkException;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\utils\MainLogger;
|
||||
use function assert;
|
||||
use function ceil;
|
||||
use function chr;
|
||||
use function fclose;
|
||||
@ -40,10 +41,12 @@ use function fseek;
|
||||
use function ftruncate;
|
||||
use function fwrite;
|
||||
use function is_resource;
|
||||
use function ksort;
|
||||
use function max;
|
||||
use function ord;
|
||||
use function pack;
|
||||
use function str_pad;
|
||||
use function str_repeat;
|
||||
use function stream_set_read_buffer;
|
||||
use function stream_set_write_buffer;
|
||||
use function strlen;
|
||||
@ -51,6 +54,7 @@ use function substr;
|
||||
use function time;
|
||||
use function touch;
|
||||
use function unpack;
|
||||
use const SORT_NUMERIC;
|
||||
use const STR_PAD_RIGHT;
|
||||
|
||||
class RegionLoader{
|
||||
@ -61,7 +65,7 @@ class RegionLoader{
|
||||
public const MAX_SECTOR_LENGTH = 255 << 12; //255 sectors (~0.996 MiB)
|
||||
public const REGION_HEADER_LENGTH = 8192; //4096 location table + 4096 timestamps
|
||||
|
||||
private const FIRST_SECTOR = 2; //location table occupies 0 and 1
|
||||
public const FIRST_SECTOR = 2; //location table occupies 0 and 1
|
||||
|
||||
/** @var int */
|
||||
public static $COMPRESSION_LEVEL = 7;
|
||||
@ -76,8 +80,10 @@ class RegionLoader{
|
||||
protected $filePointer;
|
||||
/** @var int */
|
||||
protected $nextSector = self::FIRST_SECTOR;
|
||||
/** @var RegionLocationTableEntry[] */
|
||||
/** @var RegionLocationTableEntry[]|null[] */
|
||||
protected $locationTable = [];
|
||||
/** @var RegionGarbageMap */
|
||||
protected $garbageTable;
|
||||
/** @var int */
|
||||
public $lastUsed = 0;
|
||||
|
||||
@ -85,6 +91,7 @@ class RegionLoader{
|
||||
$this->x = $regionX;
|
||||
$this->z = $regionZ;
|
||||
$this->filePath = $filePath;
|
||||
$this->garbageTable = new RegionGarbageMap([]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,7 +128,7 @@ class RegionLoader{
|
||||
}
|
||||
|
||||
protected function isChunkGenerated(int $index) : bool{
|
||||
return !$this->locationTable[$index]->isNull();
|
||||
return $this->locationTable[$index] !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,7 +140,7 @@ class RegionLoader{
|
||||
|
||||
$this->lastUsed = time();
|
||||
|
||||
if(!$this->isChunkGenerated($index)){
|
||||
if($this->locationTable[$index] === null){
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -194,19 +201,50 @@ class RegionLoader{
|
||||
|
||||
$newSize = (int) ceil(($length + 4) / 4096);
|
||||
$index = self::getChunkOffset($x, $z);
|
||||
$offset = $this->locationTable[$index]->getFirstSector();
|
||||
|
||||
if($this->locationTable[$index]->getSectorCount() < $newSize){
|
||||
$offset = $this->nextSector;
|
||||
/*
|
||||
* look for an unused area big enough to hold this data
|
||||
* this is corruption-resistant (it leaves the old data intact if a failure occurs when writing new data), and
|
||||
* also allows the file to become more compact across consecutive writes without introducing a dedicated garbage
|
||||
* collection mechanism.
|
||||
*/
|
||||
$newLocation = $this->garbageTable->allocate($newSize);
|
||||
|
||||
/* if no gaps big enough were found, append to the end of the file instead */
|
||||
if($newLocation === null){
|
||||
$newLocation = new RegionLocationTableEntry($this->nextSector, $newSize, time());
|
||||
$this->bumpNextFreeSector($newLocation);
|
||||
}
|
||||
|
||||
$this->locationTable[$index] = new RegionLocationTableEntry($offset, $newSize, time());
|
||||
$this->bumpNextFreeSector($this->locationTable[$index]);
|
||||
|
||||
fseek($this->filePointer, $offset << 12);
|
||||
/* write the chunk data into the chosen location */
|
||||
fseek($this->filePointer, $newLocation->getFirstSector() << 12);
|
||||
fwrite($this->filePointer, str_pad(Binary::writeInt($length) . chr(self::COMPRESSION_ZLIB) . $chunkData, $newSize << 12, "\x00", STR_PAD_RIGHT));
|
||||
|
||||
/*
|
||||
* update the file header - we do this after writing the main data, so that if a failure occurs while writing,
|
||||
* the header will still point to the old (intact) copy of the chunk, instead of a potentially broken new
|
||||
* version of the file (e.g. partially written).
|
||||
*/
|
||||
$oldLocation = $this->locationTable[$index];
|
||||
$this->locationTable[$index] = $newLocation;
|
||||
$this->writeLocationIndex($index);
|
||||
|
||||
if($oldLocation !== null){
|
||||
/* release the area containing the old copy to the garbage pool */
|
||||
$this->garbageTable->add($oldLocation);
|
||||
|
||||
$endGarbage = $this->garbageTable->end();
|
||||
$nextSector = $this->nextSector;
|
||||
for(; $endGarbage !== null and $endGarbage->getLastSector() + 1 === $nextSector; $endGarbage = $this->garbageTable->end()){
|
||||
$nextSector = $endGarbage->getFirstSector();
|
||||
$this->garbageTable->remove($endGarbage);
|
||||
}
|
||||
|
||||
if($nextSector !== $this->nextSector){
|
||||
$this->nextSector = $nextSector;
|
||||
ftruncate($this->filePointer, $this->nextSector << 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,7 +253,7 @@ class RegionLoader{
|
||||
*/
|
||||
public function removeChunk(int $x, int $z){
|
||||
$index = self::getChunkOffset($x, $z);
|
||||
$this->locationTable[$index] = new RegionLocationTableEntry(0, 0, 0);
|
||||
$this->locationTable[$index] = null;
|
||||
$this->writeLocationIndex($index);
|
||||
}
|
||||
|
||||
@ -261,8 +299,8 @@ class RegionLoader{
|
||||
fseek($this->filePointer, 0);
|
||||
|
||||
$headerRaw = fread($this->filePointer, self::REGION_HEADER_LENGTH);
|
||||
if(($len = strlen($headerRaw)) !== self::REGION_HEADER_LENGTH){
|
||||
throw new CorruptedRegionException("Invalid region file header, expected " . self::REGION_HEADER_LENGTH . " bytes, got " . $len . " bytes");
|
||||
if($headerRaw === false or strlen($headerRaw) !== self::REGION_HEADER_LENGTH){
|
||||
throw new CorruptedRegionException("Corrupted region header (unexpected end of file)");
|
||||
}
|
||||
|
||||
$data = unpack("N*", $headerRaw);
|
||||
@ -270,18 +308,23 @@ class RegionLoader{
|
||||
for($i = 0; $i < 1024; ++$i){
|
||||
$index = $data[$i + 1];
|
||||
$offset = $index >> 8;
|
||||
$sectorCount = $index & 0xff;
|
||||
$timestamp = $data[$i + 1025];
|
||||
|
||||
if($offset === 0){
|
||||
$this->locationTable[$i] = new RegionLocationTableEntry(0, 0, 0);
|
||||
if($offset === 0 or $sectorCount === 0){
|
||||
$this->locationTable[$i] = null;
|
||||
}elseif($offset >= self::FIRST_SECTOR){
|
||||
$this->bumpNextFreeSector($this->locationTable[$i] = new RegionLocationTableEntry($offset, $sectorCount, $timestamp));
|
||||
}else{
|
||||
$this->locationTable[$i] = new RegionLocationTableEntry($offset, $index & 0xff, $timestamp);
|
||||
$this->bumpNextFreeSector($this->locationTable[$i]);
|
||||
self::getChunkCoords($i, $chunkXX, $chunkZZ);
|
||||
throw new CorruptedRegionException("Invalid region header entry for x=$chunkXX z=$chunkZZ, offset overlaps with header");
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkLocationTableValidity();
|
||||
|
||||
$this->garbageTable = RegionGarbageMap::buildFromLocationTable($this->locationTable);
|
||||
|
||||
fseek($this->filePointer, 0);
|
||||
}
|
||||
|
||||
@ -294,7 +337,7 @@ class RegionLoader{
|
||||
|
||||
for($i = 0; $i < 1024; ++$i){
|
||||
$entry = $this->locationTable[$i];
|
||||
if($entry->isNull()){
|
||||
if($entry === null){
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -314,16 +357,34 @@ class RegionLoader{
|
||||
}
|
||||
$usedOffsets[$offset] = $i;
|
||||
}
|
||||
ksort($usedOffsets, SORT_NUMERIC);
|
||||
$prevLocationIndex = null;
|
||||
foreach($usedOffsets as $startOffset => $locationTableIndex){
|
||||
if($this->locationTable[$locationTableIndex] === null){
|
||||
continue;
|
||||
}
|
||||
if($prevLocationIndex !== null){
|
||||
assert($this->locationTable[$prevLocationIndex] !== null);
|
||||
if($this->locationTable[$locationTableIndex]->overlaps($this->locationTable[$prevLocationIndex])){
|
||||
self::getChunkCoords($locationTableIndex, $chunkXX, $chunkZZ);
|
||||
self::getChunkCoords($prevLocationIndex, $prevChunkXX, $prevChunkZZ);
|
||||
throw new CorruptedRegionException("Overlapping chunks detected in region header (chunk1: x=$chunkXX,z=$chunkZZ, chunk2: x=$prevChunkXX,z=$prevChunkZZ)");
|
||||
}
|
||||
}
|
||||
$prevLocationIndex = $locationTableIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private function writeLocationTable() : void{
|
||||
$write = [];
|
||||
|
||||
for($i = 0; $i < 1024; ++$i){
|
||||
$write[] = (($this->locationTable[$i]->getFirstSector() << 8) | $this->locationTable[$i]->getSectorCount());
|
||||
$entry = $this->locationTable[$i];
|
||||
$write[] = $entry !== null ? (($entry->getFirstSector() << 8) | $entry->getSectorCount()) : 0;
|
||||
}
|
||||
for($i = 0; $i < 1024; ++$i){
|
||||
$write[] = $this->locationTable[$i]->getTimestamp();
|
||||
$entry = $this->locationTable[$i];
|
||||
$write[] = $entry !== null ? $entry->getTimestamp() : 0;
|
||||
}
|
||||
fseek($this->filePointer, 0);
|
||||
fwrite($this->filePointer, pack("N*", ...$write), 4096 * 2);
|
||||
@ -335,10 +396,11 @@ class RegionLoader{
|
||||
* @return void
|
||||
*/
|
||||
protected function writeLocationIndex($index){
|
||||
$entry = $this->locationTable[$index];
|
||||
fseek($this->filePointer, $index << 2);
|
||||
fwrite($this->filePointer, Binary::writeInt(($this->locationTable[$index]->getFirstSector() << 8) | $this->locationTable[$index]->getSectorCount()), 4);
|
||||
fwrite($this->filePointer, Binary::writeInt($entry !== null ? ($entry->getFirstSector() << 8) | $entry->getSectorCount() : 0), 4);
|
||||
fseek($this->filePointer, 4096 + ($index << 2));
|
||||
fwrite($this->filePointer, Binary::writeInt($this->locationTable[$index]->getTimestamp()), 4);
|
||||
fwrite($this->filePointer, Binary::writeInt($entry !== null ? $entry->getTimestamp() : 0), 4);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -348,12 +410,48 @@ class RegionLoader{
|
||||
fseek($this->filePointer, 0);
|
||||
ftruncate($this->filePointer, 8192); // this fills the file with the null byte
|
||||
for($i = 0; $i < 1024; ++$i){
|
||||
$this->locationTable[$i] = new RegionLocationTableEntry(0, 0, 0);
|
||||
$this->locationTable[$i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function bumpNextFreeSector(RegionLocationTableEntry $entry) : void{
|
||||
$this->nextSector = max($this->nextSector, $entry->getLastSector()) + 1;
|
||||
$this->nextSector = max($this->nextSector, $entry->getLastSector() + 1);
|
||||
}
|
||||
|
||||
public function generateSectorMap(string $usedChar, string $freeChar) : string{
|
||||
$result = str_repeat($freeChar, $this->nextSector);
|
||||
for($i = 0; $i < self::FIRST_SECTOR; ++$i){
|
||||
$result[$i] = $usedChar;
|
||||
}
|
||||
foreach($this->locationTable as $locationTableEntry){
|
||||
if($locationTableEntry === null){
|
||||
continue;
|
||||
}
|
||||
foreach($locationTableEntry->getUsedSectors() as $sectorIndex){
|
||||
if($sectorIndex >= strlen($result)){
|
||||
throw new AssumptionFailedError("This should never happen...");
|
||||
}
|
||||
if($result[$sectorIndex] === $usedChar){
|
||||
throw new AssumptionFailedError("Overlap detected");
|
||||
}
|
||||
$result[$sectorIndex] = $usedChar;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a float between 0 and 1 indicating what fraction of the file is currently unused space.
|
||||
*/
|
||||
public function getProportionUnusedSpace() : float{
|
||||
$size = $this->nextSector;
|
||||
$used = self::FIRST_SECTOR; //header is always allocated
|
||||
foreach($this->locationTable as $entry){
|
||||
if($entry !== null){
|
||||
$used += $entry->getSectorCount();
|
||||
}
|
||||
}
|
||||
return 1 - ($used / $size);
|
||||
}
|
||||
|
||||
public function getX() : int{
|
||||
|
@ -38,12 +38,12 @@ class RegionLocationTableEntry{
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(int $firstSector, int $sectorCount, int $timestamp){
|
||||
if($firstSector < 0){
|
||||
if($firstSector < 0 or $firstSector >= 2 ** 24){
|
||||
throw new \InvalidArgumentException("Start sector must be positive, got $firstSector");
|
||||
}
|
||||
$this->firstSector = $firstSector;
|
||||
if($sectorCount < 0 or $sectorCount > 255){
|
||||
throw new \InvalidArgumentException("Sector count must be in range 0...255, got $sectorCount");
|
||||
if($sectorCount < 1){
|
||||
throw new \InvalidArgumentException("Sector count must be positive, got $sectorCount");
|
||||
}
|
||||
$this->sectorCount = $sectorCount;
|
||||
$this->timestamp = $timestamp;
|
||||
@ -73,7 +73,16 @@ class RegionLocationTableEntry{
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
public function isNull() : bool{
|
||||
return $this->firstSector === 0 or $this->sectorCount === 0;
|
||||
public function overlaps(RegionLocationTableEntry $other) : bool{
|
||||
$overlapCheck = static function(RegionLocationTableEntry $entry1, RegionLocationTableEntry $entry2) : bool{
|
||||
$entry1Last = $entry1->getLastSector();
|
||||
$entry2Last = $entry2->getLastSector();
|
||||
|
||||
return (
|
||||
($entry2->firstSector >= $entry1->firstSector and $entry2->firstSector <= $entry1Last) or
|
||||
($entry2Last >= $entry1->firstSector and $entry2Last <= $entry1Last)
|
||||
);
|
||||
};
|
||||
return $overlapCheck($this, $other) or $overlapCheck($other, $this);
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,10 @@ abstract class LightUpdate{
|
||||
*/
|
||||
protected $updateNodes = [];
|
||||
|
||||
/** @var \SplQueue */
|
||||
/**
|
||||
* @var \SplQueue
|
||||
* @phpstan-var \SplQueue<array{int, int, int}>
|
||||
*/
|
||||
protected $spreadQueue;
|
||||
/**
|
||||
* @var true[]
|
||||
@ -48,7 +51,10 @@ abstract class LightUpdate{
|
||||
*/
|
||||
protected $spreadVisited = [];
|
||||
|
||||
/** @var \SplQueue */
|
||||
/**
|
||||
* @var \SplQueue
|
||||
* @phpstan-var \SplQueue<array{int, int, int, int}>
|
||||
*/
|
||||
protected $removalQueue;
|
||||
/**
|
||||
* @var true[]
|
||||
|
@ -88,11 +88,10 @@ class NetworkBinaryStream extends BinaryStream{
|
||||
$animationCount = $this->getLInt();
|
||||
$animations = [];
|
||||
for($i = 0; $i < $animationCount; ++$i){
|
||||
$animations[] = new SkinAnimation(
|
||||
$skinImage = $this->getSkinImage(),
|
||||
$animationType = $this->getLInt(),
|
||||
$animationFrames = $this->getLFloat()
|
||||
);
|
||||
$skinImage = $this->getSkinImage();
|
||||
$animationType = $this->getLInt();
|
||||
$animationFrames = $this->getLFloat();
|
||||
$animations[] = new SkinAnimation($skinImage, $animationType, $animationFrames);
|
||||
}
|
||||
$capeData = $this->getSkinImage();
|
||||
$geometryData = $this->getString();
|
||||
@ -107,13 +106,12 @@ class NetworkBinaryStream extends BinaryStream{
|
||||
$personaPieceCount = $this->getLInt();
|
||||
$personaPieces = [];
|
||||
for($i = 0; $i < $personaPieceCount; ++$i){
|
||||
$personaPieces[] = new PersonaSkinPiece(
|
||||
$pieceId = $this->getString(),
|
||||
$pieceType = $this->getString(),
|
||||
$packId = $this->getString(),
|
||||
$isDefaultPiece = $this->getBool(),
|
||||
$productId = $this->getString()
|
||||
);
|
||||
$pieceId = $this->getString();
|
||||
$pieceType = $this->getString();
|
||||
$packId = $this->getString();
|
||||
$isDefaultPiece = $this->getBool();
|
||||
$productId = $this->getString();
|
||||
$personaPieces[] = new PersonaSkinPiece($pieceId, $pieceType, $packId, $isDefaultPiece, $productId);
|
||||
}
|
||||
$pieceTintColorCount = $this->getLInt();
|
||||
$pieceTintColors = [];
|
||||
@ -202,9 +200,9 @@ class NetworkBinaryStream extends BinaryStream{
|
||||
/** @var CompoundTag|null $nbt */
|
||||
$nbt = null;
|
||||
if($nbtLen === 0xffff){
|
||||
$c = $this->getByte();
|
||||
if($c !== 1){
|
||||
throw new \UnexpectedValueException("Unexpected NBT count $c");
|
||||
$nbtDataVersion = $this->getByte();
|
||||
if($nbtDataVersion !== 1){
|
||||
throw new \UnexpectedValueException("Unexpected NBT data version $nbtDataVersion");
|
||||
}
|
||||
$decodedNBT = (new NetworkLittleEndianNBTStream())->read($this->buffer, false, $this->offset, 512);
|
||||
if(!($decodedNBT instanceof CompoundTag)){
|
||||
@ -277,7 +275,7 @@ class NetworkBinaryStream extends BinaryStream{
|
||||
|
||||
if($nbt !== null){
|
||||
$this->putLShort(0xffff);
|
||||
$this->putByte(1); //TODO: some kind of count field? always 1 as of 1.9.0
|
||||
$this->putByte(1); //TODO: NBT data version (?)
|
||||
$this->put((new NetworkLittleEndianNBTStream())->write($nbt));
|
||||
}else{
|
||||
$this->putLShort(0);
|
||||
@ -473,7 +471,7 @@ class NetworkBinaryStream extends BinaryStream{
|
||||
/**
|
||||
* Reads and returns an EntityUniqueID
|
||||
*/
|
||||
public function getEntityUniqueId() : int{
|
||||
final public function getEntityUniqueId() : int{
|
||||
return $this->getVarLong();
|
||||
}
|
||||
|
||||
@ -487,7 +485,7 @@ class NetworkBinaryStream extends BinaryStream{
|
||||
/**
|
||||
* Reads and returns an EntityRuntimeID
|
||||
*/
|
||||
public function getEntityRuntimeId() : int{
|
||||
final public function getEntityRuntimeId() : int{
|
||||
return $this->getUnsignedVarLong();
|
||||
}
|
||||
|
||||
@ -546,11 +544,10 @@ class NetworkBinaryStream extends BinaryStream{
|
||||
* Reads a floating-point Vector3 object with coordinates rounded to 4 decimal places.
|
||||
*/
|
||||
public function getVector3() : Vector3{
|
||||
return new Vector3(
|
||||
$this->getLFloat(),
|
||||
$this->getLFloat(),
|
||||
$this->getLFloat()
|
||||
);
|
||||
$x = $this->getLFloat();
|
||||
$y = $this->getLFloat();
|
||||
$z = $this->getLFloat();
|
||||
return new Vector3($x, $y, $z);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -647,14 +644,12 @@ class NetworkBinaryStream extends BinaryStream{
|
||||
}
|
||||
|
||||
protected function getEntityLink() : EntityLink{
|
||||
$link = new EntityLink();
|
||||
|
||||
$link->fromEntityUniqueId = $this->getEntityUniqueId();
|
||||
$link->toEntityUniqueId = $this->getEntityUniqueId();
|
||||
$link->type = $this->getByte();
|
||||
$link->immediate = $this->getBool();
|
||||
|
||||
return $link;
|
||||
$fromEntityUniqueId = $this->getEntityUniqueId();
|
||||
$toEntityUniqueId = $this->getEntityUniqueId();
|
||||
$type = $this->getByte();
|
||||
$immediate = $this->getBool();
|
||||
$causedByRider = $this->getBool();
|
||||
return new EntityLink($fromEntityUniqueId, $toEntityUniqueId, $type, $immediate, $causedByRider);
|
||||
}
|
||||
|
||||
protected function putEntityLink(EntityLink $link) : void{
|
||||
@ -662,6 +657,7 @@ class NetworkBinaryStream extends BinaryStream{
|
||||
$this->putEntityUniqueId($link->toEntityUniqueId);
|
||||
$this->putByte($link->type);
|
||||
$this->putBool($link->immediate);
|
||||
$this->putBool($link->causedByRider);
|
||||
}
|
||||
|
||||
protected function getCommandOriginData() : CommandOriginData{
|
||||
@ -753,4 +749,12 @@ class NetworkBinaryStream extends BinaryStream{
|
||||
$this->putStructureSettings($structureEditorData->structureSettings);
|
||||
$this->putVarInt($structureEditorData->structureRedstoneSaveMove);
|
||||
}
|
||||
|
||||
public function readGenericTypeNetworkId() : int{
|
||||
return $this->getVarInt();
|
||||
}
|
||||
|
||||
public function writeGenericTypeNetworkId(int $id) : void{
|
||||
$this->putVarInt($id);
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ use pocketmine\network\mcpe\protocol\ClientCacheBlobStatusPacket;
|
||||
use pocketmine\network\mcpe\protocol\ClientCacheMissResponsePacket;
|
||||
use pocketmine\network\mcpe\protocol\ClientCacheStatusPacket;
|
||||
use pocketmine\network\mcpe\protocol\ClientToServerHandshakePacket;
|
||||
use pocketmine\network\mcpe\protocol\CodeBuilderPacket;
|
||||
use pocketmine\network\mcpe\protocol\CommandBlockUpdatePacket;
|
||||
use pocketmine\network\mcpe\protocol\CommandOutputPacket;
|
||||
use pocketmine\network\mcpe\protocol\CommandRequestPacket;
|
||||
@ -61,9 +62,12 @@ use pocketmine\network\mcpe\protocol\ContainerOpenPacket;
|
||||
use pocketmine\network\mcpe\protocol\ContainerSetDataPacket;
|
||||
use pocketmine\network\mcpe\protocol\CraftingDataPacket;
|
||||
use pocketmine\network\mcpe\protocol\CraftingEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\CreativeContentPacket;
|
||||
use pocketmine\network\mcpe\protocol\DataPacket;
|
||||
use pocketmine\network\mcpe\protocol\DebugInfoPacket;
|
||||
use pocketmine\network\mcpe\protocol\DisconnectPacket;
|
||||
use pocketmine\network\mcpe\protocol\EducationSettingsPacket;
|
||||
use pocketmine\network\mcpe\protocol\EmoteListPacket;
|
||||
use pocketmine\network\mcpe\protocol\EmotePacket;
|
||||
use pocketmine\network\mcpe\protocol\EventPacket;
|
||||
use pocketmine\network\mcpe\protocol\GameRulesChangedPacket;
|
||||
@ -74,6 +78,8 @@ use pocketmine\network\mcpe\protocol\InventoryContentPacket;
|
||||
use pocketmine\network\mcpe\protocol\InventorySlotPacket;
|
||||
use pocketmine\network\mcpe\protocol\InventoryTransactionPacket;
|
||||
use pocketmine\network\mcpe\protocol\ItemFrameDropItemPacket;
|
||||
use pocketmine\network\mcpe\protocol\ItemStackRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\ItemStackResponsePacket;
|
||||
use pocketmine\network\mcpe\protocol\LabTablePacket;
|
||||
use pocketmine\network\mcpe\protocol\LecternUpdatePacket;
|
||||
use pocketmine\network\mcpe\protocol\LevelChunkPacket;
|
||||
@ -99,15 +105,20 @@ use pocketmine\network\mcpe\protocol\NetworkSettingsPacket;
|
||||
use pocketmine\network\mcpe\protocol\NetworkStackLatencyPacket;
|
||||
use pocketmine\network\mcpe\protocol\NpcRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\OnScreenTextureAnimationPacket;
|
||||
use pocketmine\network\mcpe\protocol\PacketViolationWarningPacket;
|
||||
use pocketmine\network\mcpe\protocol\PhotoTransferPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerActionPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerArmorDamagePacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerAuthInputPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerEnchantOptionsPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerHotbarPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerInputPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerListPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerSkinPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlaySoundPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
|
||||
use pocketmine\network\mcpe\protocol\PositionTrackingDBClientRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\PositionTrackingDBServerBroadcastPacket;
|
||||
use pocketmine\network\mcpe\protocol\PurchaseReceiptPacket;
|
||||
use pocketmine\network\mcpe\protocol\RemoveActorPacket;
|
||||
use pocketmine\network\mcpe\protocol\RemoveEntityPacket;
|
||||
@ -163,9 +174,9 @@ use pocketmine\network\mcpe\protocol\UpdateBlockPacket;
|
||||
use pocketmine\network\mcpe\protocol\UpdateBlockPropertiesPacket;
|
||||
use pocketmine\network\mcpe\protocol\UpdateBlockSyncedPacket;
|
||||
use pocketmine\network\mcpe\protocol\UpdateEquipPacket;
|
||||
use pocketmine\network\mcpe\protocol\UpdatePlayerGameTypePacket;
|
||||
use pocketmine\network\mcpe\protocol\UpdateSoftEnumPacket;
|
||||
use pocketmine\network\mcpe\protocol\UpdateTradePacket;
|
||||
use pocketmine\network\mcpe\protocol\VideoStreamConnectPacket;
|
||||
|
||||
abstract class NetworkSession{
|
||||
|
||||
@ -666,10 +677,6 @@ abstract class NetworkSession{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleVideoStreamConnect(VideoStreamConnectPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleAddEntity(AddEntityPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
@ -741,4 +748,52 @@ abstract class NetworkSession{
|
||||
public function handlePlayerAuthInput(PlayerAuthInputPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleCreativeContent(CreativeContentPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handlePlayerEnchantOptions(PlayerEnchantOptionsPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleItemStackRequest(ItemStackRequestPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleItemStackResponse(ItemStackResponsePacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handlePlayerArmorDamage(PlayerArmorDamagePacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleCodeBuilder(CodeBuilderPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleUpdatePlayerGameType(UpdatePlayerGameTypePacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleEmoteList(EmoteListPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handlePositionTrackingDBServerBroadcast(PositionTrackingDBServerBroadcastPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handlePositionTrackingDBClientRequest(PositionTrackingDBClientRequestPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleDebugInfo(DebugInfoPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handlePacketViolationWarning(PacketViolationWarningPacket $packet) : bool{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ class RakLibInterface implements ServerInstance, AdvancedSourceInterface{
|
||||
* Sometimes this gets changed when the MCPE-layer protocol gets broken to the point where old and new can't
|
||||
* communicate. It's important that we check this to avoid catastrophes.
|
||||
*/
|
||||
private const MCPE_RAKNET_PROTOCOL_VERSION = 9;
|
||||
private const MCPE_RAKNET_PROTOCOL_VERSION = 10;
|
||||
|
||||
/** @var Server */
|
||||
private $server;
|
||||
|
@ -35,10 +35,12 @@ use function json_decode;
|
||||
use function ltrim;
|
||||
use function openssl_verify;
|
||||
use function ord;
|
||||
use function serialize;
|
||||
use function str_split;
|
||||
use function strlen;
|
||||
use function strtr;
|
||||
use function time;
|
||||
use function unserialize;
|
||||
use function wordwrap;
|
||||
use const OPENSSL_ALGO_SHA384;
|
||||
|
||||
@ -48,8 +50,10 @@ class VerifyLoginTask extends AsyncTask{
|
||||
|
||||
private const CLOCK_DRIFT_MAX = 60;
|
||||
|
||||
/** @var LoginPacket */
|
||||
private $packet;
|
||||
/** @var string */
|
||||
private $chainJwts;
|
||||
/** @var string */
|
||||
private $clientDataJwt;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
@ -66,23 +70,25 @@ class VerifyLoginTask extends AsyncTask{
|
||||
private $authenticated = false;
|
||||
|
||||
public function __construct(Player $player, LoginPacket $packet){
|
||||
$this->storeLocal($player);
|
||||
$this->packet = $packet;
|
||||
$this->storeLocal([$player, $packet]);
|
||||
$this->chainJwts = serialize($packet->chainData["chain"]);
|
||||
$this->clientDataJwt = $packet->clientDataJwt;
|
||||
}
|
||||
|
||||
public function onRun(){
|
||||
$packet = $this->packet; //Get it in a local variable to make sure it stays unserialized
|
||||
/** @var string[] $chainJwts */
|
||||
$chainJwts = unserialize($this->chainJwts); //Get it in a local variable to make sure it stays unserialized
|
||||
|
||||
try{
|
||||
$currentKey = null;
|
||||
$first = true;
|
||||
|
||||
foreach($packet->chainData["chain"] as $jwt){
|
||||
foreach($chainJwts as $jwt){
|
||||
$this->validateToken($jwt, $currentKey, $first);
|
||||
$first = false;
|
||||
}
|
||||
|
||||
$this->validateToken($packet->clientDataJwt, $currentKey);
|
||||
$this->validateToken($this->clientDataJwt, $currentKey);
|
||||
|
||||
$this->error = null;
|
||||
}catch(VerifyLoginException $e){
|
||||
@ -109,6 +115,9 @@ class VerifyLoginTask extends AsyncTask{
|
||||
|
||||
//First link, check that it is self-signed
|
||||
$currentPublicKey = $headers["x5u"];
|
||||
}elseif($headers["x5u"] !== $currentPublicKey){
|
||||
//Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
|
||||
throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.badSignature");
|
||||
}
|
||||
|
||||
$plainSignature = base64_decode(strtr($sigB64, '-_', '+/'), true);
|
||||
@ -160,12 +169,15 @@ class VerifyLoginTask extends AsyncTask{
|
||||
}
|
||||
|
||||
public function onCompletion(Server $server){
|
||||
/** @var Player $player */
|
||||
$player = $this->fetchLocal();
|
||||
/**
|
||||
* @var Player $player
|
||||
* @var LoginPacket $packet
|
||||
*/
|
||||
[$player, $packet] = $this->fetchLocal();
|
||||
if(!$player->isConnected()){
|
||||
$server->getLogger()->error("Player " . $player->getName() . " was disconnected before their login could be verified");
|
||||
}else{
|
||||
$player->onVerifyCompleted($this->packet, $this->error, $this->authenticated);
|
||||
$player->onVerifyCompleted($packet, $this->error, $this->authenticated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
<?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\network\mcpe\convert;
|
||||
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
|
||||
final class R12ToCurrentBlockMapEntry{
|
||||
|
||||
/** @var string */
|
||||
private $id;
|
||||
/** @var int */
|
||||
private $meta;
|
||||
/** @var CompoundTag */
|
||||
private $blockState;
|
||||
|
||||
public function __construct(string $id, int $meta, CompoundTag $blockState){
|
||||
$this->id = $id;
|
||||
$this->meta = $meta;
|
||||
$this->blockState = $blockState;
|
||||
}
|
||||
|
||||
public function getId() : string{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getMeta() : int{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
public function getBlockState() : CompoundTag{
|
||||
return $this->blockState;
|
||||
}
|
||||
|
||||
public function __toString(){
|
||||
return "id=$this->id, meta=$this->meta, nbt=$this->blockState";
|
||||
}
|
||||
}
|
@ -21,13 +21,14 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\protocol\types;
|
||||
namespace pocketmine\network\mcpe\convert;
|
||||
|
||||
use pocketmine\block\BlockIds;
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\NetworkLittleEndianNBTStream;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\network\mcpe\NetworkBinaryStream;
|
||||
use function file_get_contents;
|
||||
use function getmypid;
|
||||
use function json_decode;
|
||||
@ -66,9 +67,22 @@ final class RuntimeBlockMapping{
|
||||
|
||||
private static function setupLegacyMappings() : void{
|
||||
$legacyIdMap = json_decode(file_get_contents(\pocketmine\RESOURCE_PATH . "vanilla/block_id_map.json"), true);
|
||||
$legacyStateMap = (new NetworkLittleEndianNBTStream())->read(file_get_contents(\pocketmine\RESOURCE_PATH . "vanilla/r12_to_current_block_map.nbt"));
|
||||
if(!($legacyStateMap instanceof ListTag) or $legacyStateMap->getTagType() !== NBT::TAG_Compound){
|
||||
throw new \RuntimeException("Invalid legacy states mapping table, expected TAG_List<TAG_Compound> root");
|
||||
|
||||
/** @var R12ToCurrentBlockMapEntry[] $legacyStateMap */
|
||||
$legacyStateMap = [];
|
||||
$legacyStateMapReader = new NetworkBinaryStream(file_get_contents(\pocketmine\RESOURCE_PATH . "vanilla/r12_to_current_block_map.bin"));
|
||||
$nbtReader = new NetworkLittleEndianNBTStream();
|
||||
while(!$legacyStateMapReader->feof()){
|
||||
$id = $legacyStateMapReader->getString();
|
||||
$meta = $legacyStateMapReader->getLShort();
|
||||
|
||||
$offset = $legacyStateMapReader->getOffset();
|
||||
$state = $nbtReader->read($legacyStateMapReader->getBuffer(), false, $offset);
|
||||
$legacyStateMapReader->setOffset($offset);
|
||||
if(!($state instanceof CompoundTag)){
|
||||
throw new \RuntimeException("Blockstate should be a TAG_Compound");
|
||||
}
|
||||
$legacyStateMap[] = new R12ToCurrentBlockMapEntry($id, $meta, $state);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,16 +92,17 @@ final class RuntimeBlockMapping{
|
||||
foreach(self::$bedrockKnownStates as $k => $state){
|
||||
$idToStatesMap[$state->getCompoundTag("block")->getString("name")][] = $k;
|
||||
}
|
||||
/** @var CompoundTag $pair */
|
||||
foreach($legacyStateMap as $pair){
|
||||
$oldState = $pair->getCompoundTag("old");
|
||||
$id = $legacyIdMap[$oldState->getString("name")];
|
||||
$data = $oldState->getShort("val");
|
||||
$id = $legacyIdMap[$pair->getId()] ?? null;
|
||||
if($id === null){
|
||||
throw new \RuntimeException("No legacy ID matches " . $pair->getId());
|
||||
}
|
||||
$data = $pair->getMeta();
|
||||
if($data > 15){
|
||||
//we can't handle metadata with more than 4 bits
|
||||
continue;
|
||||
}
|
||||
$mappedState = $pair->getCompoundTag("new");
|
||||
$mappedState = $pair->getBlockState();
|
||||
|
||||
//TODO HACK: idiotic NBT compare behaviour on 3.x compares keys which are stored by values
|
||||
$mappedState->setName("block");
|
@ -87,6 +87,7 @@ class ActorEventPacket extends DataPacket{
|
||||
public const TREASURE_HUNT = 72;
|
||||
public const AGENT_SUMMON = 73;
|
||||
public const CHARGED_CROSSBOW = 74;
|
||||
public const FALL = 75;
|
||||
|
||||
//TODO: add more events
|
||||
|
||||
|
@ -32,6 +32,7 @@ use function get_class;
|
||||
use function strlen;
|
||||
use function zlib_decode;
|
||||
use function zlib_encode;
|
||||
use const ZLIB_ENCODING_RAW;
|
||||
#ifndef COMPILE
|
||||
use pocketmine\utils\Binary;
|
||||
#endif
|
||||
@ -71,7 +72,7 @@ class BatchPacket extends DataPacket{
|
||||
}
|
||||
|
||||
protected function encodePayload(){
|
||||
$this->put(zlib_encode($this->payload, ZLIB_ENCODING_DEFLATE, $this->compressionLevel));
|
||||
$this->put(zlib_encode($this->payload, ZLIB_ENCODING_RAW, $this->compressionLevel));
|
||||
}
|
||||
|
||||
/**
|
||||
|
66
src/pocketmine/network/mcpe/protocol/CodeBuilderPacket.php
Normal file
66
src/pocketmine/network/mcpe/protocol/CodeBuilderPacket.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?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\network\mcpe\protocol;
|
||||
|
||||
#include <rules/DataPacket.h>
|
||||
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
|
||||
class CodeBuilderPacket extends DataPacket/* implements ClientboundPacket*/{
|
||||
public const NETWORK_ID = ProtocolInfo::CODE_BUILDER_PACKET;
|
||||
|
||||
/** @var string */
|
||||
private $url;
|
||||
/** @var bool */
|
||||
private $openCodeBuilder;
|
||||
|
||||
public static function create(string $url, bool $openCodeBuilder) : self{
|
||||
$result = new self;
|
||||
$result->url = $url;
|
||||
$result->openCodeBuilder = $openCodeBuilder;
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getUrl() : string{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function openCodeBuilder() : bool{
|
||||
return $this->openCodeBuilder;
|
||||
}
|
||||
|
||||
protected function decodePayload() : void{
|
||||
$this->url = $this->getString();
|
||||
$this->openCodeBuilder = $this->getBool();
|
||||
}
|
||||
|
||||
protected function encodePayload() : void{
|
||||
$this->putString($this->url);
|
||||
$this->putBool($this->openCodeBuilder);
|
||||
}
|
||||
|
||||
public function handle(NetworkSession $handler) : bool{
|
||||
return $handler->handleCodeBuilder($this);
|
||||
}
|
||||
}
|
@ -97,6 +97,7 @@ class CraftingDataPacket extends DataPacket{
|
||||
$entry["uuid"] = $this->getUUID()->toString();
|
||||
$entry["block"] = $this->getString();
|
||||
$entry["priority"] = $this->getVarInt();
|
||||
$entry["net_id"] = $this->readGenericTypeNetworkId();
|
||||
|
||||
break;
|
||||
case self::ENTRY_SHAPED:
|
||||
@ -118,6 +119,7 @@ class CraftingDataPacket extends DataPacket{
|
||||
$entry["uuid"] = $this->getUUID()->toString();
|
||||
$entry["block"] = $this->getString();
|
||||
$entry["priority"] = $this->getVarInt();
|
||||
$entry["net_id"] = $this->readGenericTypeNetworkId();
|
||||
|
||||
break;
|
||||
case self::ENTRY_FURNACE:
|
||||
@ -140,6 +142,7 @@ class CraftingDataPacket extends DataPacket{
|
||||
break;
|
||||
case self::ENTRY_MULTI:
|
||||
$entry["uuid"] = $this->getUUID()->toString();
|
||||
$entry["net_id"] = $this->readGenericTypeNetworkId();
|
||||
break;
|
||||
default:
|
||||
throw new \UnexpectedValueException("Unhandled recipe type $recipeType!"); //do not continue attempting to decode
|
||||
@ -148,9 +151,12 @@ class CraftingDataPacket extends DataPacket{
|
||||
}
|
||||
for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){
|
||||
$input = $this->getVarInt();
|
||||
$inputMeta = $this->getVarInt();
|
||||
$ingredient = $this->getVarInt();
|
||||
$ingredientMeta = $this->getVarInt();
|
||||
$output = $this->getVarInt();
|
||||
$this->potionTypeRecipes[] = new PotionTypeRecipe($input, $ingredient, $output);
|
||||
$outputMeta = $this->getVarInt();
|
||||
$this->potionTypeRecipes[] = new PotionTypeRecipe($input, $inputMeta, $ingredient, $ingredientMeta, $output, $outputMeta);
|
||||
}
|
||||
for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){
|
||||
$input = $this->getVarInt();
|
||||
@ -193,6 +199,7 @@ class CraftingDataPacket extends DataPacket{
|
||||
$stream->put(str_repeat("\x00", 16)); //Null UUID
|
||||
$stream->putString("crafting_table"); //TODO: blocktype (no prefix) (this might require internal API breaks)
|
||||
$stream->putVarInt(50); //TODO: priority
|
||||
$stream->writeGenericTypeNetworkId($pos); //TODO: ANOTHER recipe ID, only used on the network
|
||||
|
||||
return CraftingDataPacket::ENTRY_SHAPELESS;
|
||||
}
|
||||
@ -217,6 +224,7 @@ class CraftingDataPacket extends DataPacket{
|
||||
$stream->put(str_repeat("\x00", 16)); //Null UUID
|
||||
$stream->putString("crafting_table"); //TODO: blocktype (no prefix) (this might require internal API breaks)
|
||||
$stream->putVarInt(50); //TODO: priority
|
||||
$stream->writeGenericTypeNetworkId($pos); //TODO: ANOTHER recipe ID, only used on the network
|
||||
|
||||
return CraftingDataPacket::ENTRY_SHAPED;
|
||||
}
|
||||
@ -272,9 +280,12 @@ class CraftingDataPacket extends DataPacket{
|
||||
}
|
||||
$this->putUnsignedVarInt(count($this->potionTypeRecipes));
|
||||
foreach($this->potionTypeRecipes as $recipe){
|
||||
$this->putVarInt($recipe->getInputPotionType());
|
||||
$this->putVarInt($recipe->getInputItemId());
|
||||
$this->putVarInt($recipe->getInputItemMeta());
|
||||
$this->putVarInt($recipe->getIngredientItemId());
|
||||
$this->putVarInt($recipe->getOutputPotionType());
|
||||
$this->putVarInt($recipe->getIngredientItemMeta());
|
||||
$this->putVarInt($recipe->getOutputItemId());
|
||||
$this->putVarInt($recipe->getOutputItemMeta());
|
||||
}
|
||||
$this->putUnsignedVarInt(count($this->potionContainerRecipes));
|
||||
foreach($this->potionContainerRecipes as $recipe){
|
||||
|
@ -0,0 +1,67 @@
|
||||
<?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\network\mcpe\protocol;
|
||||
|
||||
#include <rules/DataPacket.h>
|
||||
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
|
||||
use function count;
|
||||
|
||||
class CreativeContentPacket extends DataPacket/* implements ClientboundPacket*/{
|
||||
public const NETWORK_ID = ProtocolInfo::CREATIVE_CONTENT_PACKET;
|
||||
|
||||
/** @var CreativeContentEntry[] */
|
||||
private $entries;
|
||||
|
||||
/**
|
||||
* @param CreativeContentEntry[] $entries
|
||||
*/
|
||||
public static function create(array $entries) : self{
|
||||
$result = new self;
|
||||
$result->entries = $entries;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @return CreativeContentEntry[] */
|
||||
public function getEntries() : array{ return $this->entries; }
|
||||
|
||||
protected function decodePayload() : void{
|
||||
$this->entries = [];
|
||||
for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){
|
||||
$this->entries[] = CreativeContentEntry::read($this);
|
||||
}
|
||||
}
|
||||
|
||||
protected function encodePayload() : void{
|
||||
$this->putUnsignedVarInt(count($this->entries));
|
||||
foreach($this->entries as $entry){
|
||||
$entry->write($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(NetworkSession $handler) : bool{
|
||||
return $handler->handleCreativeContent($this);
|
||||
}
|
||||
}
|
65
src/pocketmine/network/mcpe/protocol/DebugInfoPacket.php
Normal file
65
src/pocketmine/network/mcpe/protocol/DebugInfoPacket.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?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\network\mcpe\protocol;
|
||||
|
||||
#include <rules/DataPacket.h>
|
||||
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
|
||||
class DebugInfoPacket extends DataPacket/* implements ClientboundPacket, ServerboundPacket*/{
|
||||
public const NETWORK_ID = ProtocolInfo::DEBUG_INFO_PACKET;
|
||||
|
||||
/** @var int */
|
||||
private $entityUniqueId;
|
||||
/** @var string */
|
||||
private $data;
|
||||
|
||||
public static function create(int $entityUniqueId, string $data) : self{
|
||||
$result = new self;
|
||||
$result->entityUniqueId = $entityUniqueId;
|
||||
$result->data = $data;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: we can't call this getEntityRuntimeId() because of base class collision (crap architecture, thanks Shoghi)
|
||||
*/
|
||||
public function getEntityUniqueIdField() : int{ return $this->entityUniqueId; }
|
||||
|
||||
public function getData() : string{ return $this->data; }
|
||||
|
||||
protected function decodePayload() : void{
|
||||
$this->entityUniqueId = $this->getEntityUniqueId();
|
||||
$this->data = $this->getString();
|
||||
}
|
||||
|
||||
protected function encodePayload() : void{
|
||||
$this->putEntityUniqueId($this->entityUniqueId);
|
||||
$this->putString($this->data);
|
||||
}
|
||||
|
||||
public function handle(NetworkSession $handler) : bool{
|
||||
return $handler->handleDebugInfo($this);
|
||||
}
|
||||
}
|
@ -32,12 +32,21 @@ class EducationSettingsPacket extends DataPacket{
|
||||
|
||||
/** @var string */
|
||||
private $codeBuilderDefaultUri;
|
||||
/** @var string */
|
||||
private $codeBuilderTitle;
|
||||
/** @var bool */
|
||||
private $canResizeCodeBuilder;
|
||||
/** @var string|null */
|
||||
private $codeBuilderOverrideUri;
|
||||
/** @var bool */
|
||||
private $hasQuiz;
|
||||
|
||||
public static function create(string $codeBuilderDefaultUri, bool $hasQuiz) : self{
|
||||
public static function create(string $codeBuilderDefaultUri, string $codeBuilderTitle, bool $canResizeCodeBuilder, ?string $codeBuilderOverrideUri, bool $hasQuiz) : self{
|
||||
$result = new self;
|
||||
$result->codeBuilderDefaultUri = $codeBuilderDefaultUri;
|
||||
$result->codeBuilderTitle = $codeBuilderTitle;
|
||||
$result->canResizeCodeBuilder = $canResizeCodeBuilder;
|
||||
$result->codeBuilderOverrideUri = $codeBuilderOverrideUri;
|
||||
$result->hasQuiz = $hasQuiz;
|
||||
return $result;
|
||||
}
|
||||
@ -46,17 +55,42 @@ class EducationSettingsPacket extends DataPacket{
|
||||
return $this->codeBuilderDefaultUri;
|
||||
}
|
||||
|
||||
public function getCodeBuilderTitle() : string{
|
||||
return $this->codeBuilderTitle;
|
||||
}
|
||||
|
||||
public function canResizeCodeBuilder() : bool{
|
||||
return $this->canResizeCodeBuilder;
|
||||
}
|
||||
|
||||
public function getCodeBuilderOverrideUri() : ?string{
|
||||
return $this->codeBuilderOverrideUri;
|
||||
}
|
||||
|
||||
public function getHasQuiz() : bool{
|
||||
return $this->hasQuiz;
|
||||
}
|
||||
|
||||
protected function decodePayload() : void{
|
||||
$this->codeBuilderDefaultUri = $this->getString();
|
||||
$this->codeBuilderTitle = $this->getString();
|
||||
$this->canResizeCodeBuilder = $this->getBool();
|
||||
if($this->getBool()){
|
||||
$this->codeBuilderOverrideUri = $this->getString();
|
||||
}else{
|
||||
$this->codeBuilderOverrideUri = null;
|
||||
}
|
||||
$this->hasQuiz = $this->getBool();
|
||||
}
|
||||
|
||||
protected function encodePayload() : void{
|
||||
$this->putString($this->codeBuilderDefaultUri);
|
||||
$this->putString($this->codeBuilderTitle);
|
||||
$this->putBool($this->canResizeCodeBuilder);
|
||||
$this->putBool($this->codeBuilderOverrideUri !== null);
|
||||
if($this->codeBuilderOverrideUri !== null){
|
||||
$this->putString($this->codeBuilderOverrideUri);
|
||||
}
|
||||
$this->putBool($this->hasQuiz);
|
||||
}
|
||||
|
||||
|
74
src/pocketmine/network/mcpe/protocol/EmoteListPacket.php
Normal file
74
src/pocketmine/network/mcpe/protocol/EmoteListPacket.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?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\network\mcpe\protocol;
|
||||
|
||||
#include <rules/DataPacket.h>
|
||||
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\utils\UUID;
|
||||
use function count;
|
||||
|
||||
class EmoteListPacket extends DataPacket/* implements ClientboundPacket*/{
|
||||
public const NETWORK_ID = ProtocolInfo::EMOTE_LIST_PACKET;
|
||||
|
||||
/** @var int */
|
||||
private $playerEntityRuntimeId;
|
||||
/** @var UUID[] */
|
||||
private $emoteIds;
|
||||
|
||||
/**
|
||||
* @param UUID[] $emoteIds
|
||||
*/
|
||||
public static function create(int $playerEntityRuntimeId, array $emoteIds) : self{
|
||||
$result = new self;
|
||||
$result->playerEntityRuntimeId = $playerEntityRuntimeId;
|
||||
$result->emoteIds = $emoteIds;
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getPlayerEntityRuntimeId() : int{ return $this->playerEntityRuntimeId; }
|
||||
|
||||
/** @return UUID[] */
|
||||
public function getEmoteIds() : array{ return $this->emoteIds; }
|
||||
|
||||
protected function decodePayload() : void{
|
||||
$this->playerEntityRuntimeId = $this->getEntityRuntimeId();
|
||||
$this->emoteIds = [];
|
||||
for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){
|
||||
$this->emoteIds[] = $this->getUUID();
|
||||
}
|
||||
}
|
||||
|
||||
protected function encodePayload() : void{
|
||||
$this->putEntityRuntimeId($this->playerEntityRuntimeId);
|
||||
$this->putUnsignedVarInt(count($this->emoteIds));
|
||||
foreach($this->emoteIds as $emoteId){
|
||||
$this->putUUID($emoteId);
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(NetworkSession $handler) : bool{
|
||||
return $handler->handleEmoteList($this);
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ use pocketmine\network\mcpe\NetworkSession;
|
||||
class EmotePacket extends DataPacket/* implements ClientboundPacket, ServerboundPacket*/{
|
||||
public const NETWORK_ID = ProtocolInfo::EMOTE_PACKET;
|
||||
|
||||
private const FLAG_SERVER = 1 << 0;
|
||||
public const FLAG_SERVER = 1 << 0;
|
||||
|
||||
/** @var int */
|
||||
private $entityRuntimeId;
|
||||
|
@ -30,14 +30,18 @@ use pocketmine\network\mcpe\NetworkSession;
|
||||
class HurtArmorPacket extends DataPacket{
|
||||
public const NETWORK_ID = ProtocolInfo::HURT_ARMOR_PACKET;
|
||||
|
||||
/** @var int */
|
||||
public $cause;
|
||||
/** @var int */
|
||||
public $health;
|
||||
|
||||
protected function decodePayload(){
|
||||
$this->cause = $this->getVarInt();
|
||||
$this->health = $this->getVarInt();
|
||||
}
|
||||
|
||||
protected function encodePayload(){
|
||||
$this->putVarInt($this->cause);
|
||||
$this->putVarInt($this->health);
|
||||
}
|
||||
|
||||
|
@ -25,8 +25,8 @@ namespace pocketmine\network\mcpe\protocol;
|
||||
|
||||
#include <rules/DataPacket.h>
|
||||
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
||||
use function count;
|
||||
|
||||
class InventoryContentPacket extends DataPacket{
|
||||
@ -34,14 +34,14 @@ class InventoryContentPacket extends DataPacket{
|
||||
|
||||
/** @var int */
|
||||
public $windowId;
|
||||
/** @var Item[] */
|
||||
/** @var ItemStackWrapper[] */
|
||||
public $items = [];
|
||||
|
||||
protected function decodePayload(){
|
||||
$this->windowId = $this->getUnsignedVarInt();
|
||||
$count = $this->getUnsignedVarInt();
|
||||
for($i = 0; $i < $count; ++$i){
|
||||
$this->items[] = $this->getSlot();
|
||||
$this->items[] = ItemStackWrapper::read($this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ class InventoryContentPacket extends DataPacket{
|
||||
$this->putUnsignedVarInt($this->windowId);
|
||||
$this->putUnsignedVarInt(count($this->items));
|
||||
foreach($this->items as $item){
|
||||
$this->putSlot($item);
|
||||
$item->write($this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,8 +25,8 @@ namespace pocketmine\network\mcpe\protocol;
|
||||
|
||||
#include <rules/DataPacket.h>
|
||||
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
||||
|
||||
class InventorySlotPacket extends DataPacket{
|
||||
public const NETWORK_ID = ProtocolInfo::INVENTORY_SLOT_PACKET;
|
||||
@ -35,19 +35,19 @@ class InventorySlotPacket extends DataPacket{
|
||||
public $windowId;
|
||||
/** @var int */
|
||||
public $inventorySlot;
|
||||
/** @var Item */
|
||||
/** @var ItemStackWrapper */
|
||||
public $item;
|
||||
|
||||
protected function decodePayload(){
|
||||
$this->windowId = $this->getUnsignedVarInt();
|
||||
$this->inventorySlot = $this->getUnsignedVarInt();
|
||||
$this->item = $this->getSlot();
|
||||
$this->item = ItemStackWrapper::read($this);
|
||||
}
|
||||
|
||||
protected function encodePayload(){
|
||||
$this->putUnsignedVarInt($this->windowId);
|
||||
$this->putUnsignedVarInt($this->inventorySlot);
|
||||
$this->putSlot($this->item);
|
||||
$this->item->write($this);
|
||||
}
|
||||
|
||||
public function handle(NetworkSession $session) : bool{
|
||||
|
@ -26,7 +26,7 @@ namespace pocketmine\network\mcpe\protocol;
|
||||
#include <rules/DataPacket.h>
|
||||
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\types\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\InventoryTransactionChangedSlotsHack;
|
||||
use pocketmine\network\mcpe\protocol\types\NetworkInventoryAction;
|
||||
use function count;
|
||||
|
||||
@ -50,20 +50,14 @@ class InventoryTransactionPacket extends DataPacket{
|
||||
public const USE_ITEM_ON_ENTITY_ACTION_ATTACK = 1;
|
||||
|
||||
/** @var int */
|
||||
public $transactionType;
|
||||
public $requestId;
|
||||
/** @var InventoryTransactionChangedSlotsHack[] */
|
||||
public $requestChangedSlots;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
* NOTE: THIS FIELD DOES NOT EXIST IN THE PROTOCOL, it's merely used for convenience for PocketMine-MP to easily
|
||||
* determine whether we're doing a crafting transaction.
|
||||
*/
|
||||
public $isCraftingPart = false;
|
||||
/**
|
||||
* @var bool
|
||||
* NOTE: THIS FIELD DOES NOT EXIST IN THE PROTOCOL, it's merely used for convenience for PocketMine-MP to easily
|
||||
* determine whether we're doing a crafting transaction.
|
||||
*/
|
||||
public $isFinalCraftingPart = false;
|
||||
/** @var int */
|
||||
public $transactionType;
|
||||
/** @var bool */
|
||||
public $hasItemStackIds;
|
||||
|
||||
/** @var NetworkInventoryAction[] */
|
||||
public $actions = [];
|
||||
@ -72,29 +66,20 @@ class InventoryTransactionPacket extends DataPacket{
|
||||
public $trData;
|
||||
|
||||
protected function decodePayload(){
|
||||
$this->requestId = $this->readGenericTypeNetworkId();
|
||||
$this->requestChangedSlots = [];
|
||||
if($this->requestId !== 0){
|
||||
for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){
|
||||
$this->requestChangedSlots[] = InventoryTransactionChangedSlotsHack::read($this);
|
||||
}
|
||||
}
|
||||
|
||||
$this->transactionType = $this->getUnsignedVarInt();
|
||||
|
||||
for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){
|
||||
$this->actions[] = $action = (new NetworkInventoryAction())->read($this);
|
||||
$this->hasItemStackIds = $this->getBool();
|
||||
|
||||
if(
|
||||
$action->sourceType === NetworkInventoryAction::SOURCE_CONTAINER and
|
||||
$action->windowId === ContainerIds::UI and
|
||||
$action->inventorySlot === 50 and
|
||||
!$action->oldItem->equalsExact($action->newItem)
|
||||
){
|
||||
$this->isCraftingPart = true;
|
||||
if(!$action->oldItem->isNull() and $action->newItem->isNull()){
|
||||
$this->isFinalCraftingPart = true;
|
||||
}
|
||||
}elseif(
|
||||
$action->sourceType === NetworkInventoryAction::SOURCE_TODO and (
|
||||
$action->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_RESULT or
|
||||
$action->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_USE_INGREDIENT
|
||||
)
|
||||
){
|
||||
$this->isCraftingPart = true;
|
||||
}
|
||||
for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){
|
||||
$this->actions[] = $action = (new NetworkInventoryAction())->read($this, $this->hasItemStackIds);
|
||||
}
|
||||
|
||||
$this->trData = new \stdClass();
|
||||
@ -134,11 +119,21 @@ class InventoryTransactionPacket extends DataPacket{
|
||||
}
|
||||
|
||||
protected function encodePayload(){
|
||||
$this->writeGenericTypeNetworkId($this->requestId);
|
||||
if($this->requestId !== 0){
|
||||
$this->putUnsignedVarInt(count($this->requestChangedSlots));
|
||||
foreach($this->requestChangedSlots as $changedSlots){
|
||||
$changedSlots->write($this);
|
||||
}
|
||||
}
|
||||
|
||||
$this->putUnsignedVarInt($this->transactionType);
|
||||
|
||||
$this->putBool($this->hasItemStackIds);
|
||||
|
||||
$this->putUnsignedVarInt(count($this->actions));
|
||||
foreach($this->actions as $action){
|
||||
$action->write($this);
|
||||
$action->write($this, $this->hasItemStackIds);
|
||||
}
|
||||
|
||||
switch($this->transactionType){
|
||||
|
@ -0,0 +1,67 @@
|
||||
<?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\network\mcpe\protocol;
|
||||
|
||||
#include <rules/DataPacket.h>
|
||||
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest;
|
||||
use function count;
|
||||
|
||||
class ItemStackRequestPacket extends DataPacket/* implements ServerboundPacket*/{
|
||||
public const NETWORK_ID = ProtocolInfo::ITEM_STACK_REQUEST_PACKET;
|
||||
|
||||
/** @var ItemStackRequest[] */
|
||||
private $requests;
|
||||
|
||||
/**
|
||||
* @param ItemStackRequest[] $requests
|
||||
*/
|
||||
public static function create(array $requests) : self{
|
||||
$result = new self;
|
||||
$result->requests = $requests;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @return ItemStackRequest[] */
|
||||
public function getRequests() : array{ return $this->requests; }
|
||||
|
||||
protected function decodePayload() : void{
|
||||
$this->requests = [];
|
||||
for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){
|
||||
$this->requests[] = ItemStackRequest::read($this);
|
||||
}
|
||||
}
|
||||
|
||||
protected function encodePayload() : void{
|
||||
$this->putUnsignedVarInt(count($this->requests));
|
||||
foreach($this->requests as $request){
|
||||
$request->write($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(NetworkSession $handler) : bool{
|
||||
return $handler->handleItemStackRequest($this);
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
<?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\network\mcpe\protocol;
|
||||
|
||||
#include <rules/DataPacket.h>
|
||||
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
|
||||
use function count;
|
||||
|
||||
class ItemStackResponsePacket extends DataPacket/* implements ClientboundPacket*/{
|
||||
public const NETWORK_ID = ProtocolInfo::ITEM_STACK_RESPONSE_PACKET;
|
||||
|
||||
/** @var ItemStackResponse[] */
|
||||
private $responses;
|
||||
|
||||
/**
|
||||
* @param ItemStackResponse[] $responses
|
||||
*/
|
||||
public static function create(array $responses) : self{
|
||||
$result = new self;
|
||||
$result->responses = $responses;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @return ItemStackResponse[] */
|
||||
public function getResponses() : array{ return $this->responses; }
|
||||
|
||||
protected function decodePayload() : void{
|
||||
$this->responses = [];
|
||||
for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){
|
||||
$this->responses[] = ItemStackResponse::read($this);
|
||||
}
|
||||
}
|
||||
|
||||
protected function encodePayload() : void{
|
||||
$this->putUnsignedVarInt(count($this->responses));
|
||||
foreach($this->responses as $response){
|
||||
$response->write($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(NetworkSession $handler) : bool{
|
||||
return $handler->handleItemStackResponse($this);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user