Merge branch 'resource-pack-ack-receipts' into minor-next

This commit is contained in:
Dylan K. Taylor 2024-03-04 15:46:31 +00:00
commit dbc7105e5b
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
7 changed files with 163 additions and 41 deletions

View File

@ -45,8 +45,8 @@
"pocketmine/log": "^0.4.0", "pocketmine/log": "^0.4.0",
"pocketmine/math": "~1.0.0", "pocketmine/math": "~1.0.0",
"pocketmine/nbt": "~1.0.0", "pocketmine/nbt": "~1.0.0",
"pocketmine/raklib": "^0.15.0", "pocketmine/raklib": "~1.1.0",
"pocketmine/raklib-ipc": "^0.2.0", "pocketmine/raklib-ipc": "~1.0.0",
"pocketmine/snooze": "^0.5.0", "pocketmine/snooze": "^0.5.0",
"ramsey/uuid": "~4.7.0", "ramsey/uuid": "~4.7.0",
"symfony/filesystem": "~6.4.0" "symfony/filesystem": "~6.4.0"

34
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "f9a2f433a86f9ea1351d6dc81b508160", "content-hash": "016eb0e55295aa5250888b4e096d8dca",
"packages": [ "packages": [
{ {
"name": "adhocore/json-comment", "name": "adhocore/json-comment",
@ -616,28 +616,28 @@
}, },
{ {
"name": "pocketmine/raklib", "name": "pocketmine/raklib",
"version": "0.15.1", "version": "1.1.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/pmmp/RakLib.git", "url": "https://github.com/pmmp/RakLib.git",
"reference": "79b7b4d1d7516dc6e322514453645ad9452b20ca" "reference": "be2783be516bf6e2872ff5c81fb9048596617b97"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/pmmp/RakLib/zipball/79b7b4d1d7516dc6e322514453645ad9452b20ca", "url": "https://api.github.com/repos/pmmp/RakLib/zipball/be2783be516bf6e2872ff5c81fb9048596617b97",
"reference": "79b7b4d1d7516dc6e322514453645ad9452b20ca", "reference": "be2783be516bf6e2872ff5c81fb9048596617b97",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-sockets": "*", "ext-sockets": "*",
"php": "^8.0", "php": "^8.1",
"php-64bit": "*", "php-64bit": "*",
"php-ipv6": "*", "php-ipv6": "*",
"pocketmine/binaryutils": "^0.2.0", "pocketmine/binaryutils": "^0.2.0",
"pocketmine/log": "^0.3.0 || ^0.4.0" "pocketmine/log": "^0.3.0 || ^0.4.0"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "1.9.17", "phpstan/phpstan": "1.10.1",
"phpstan/phpstan-strict-rules": "^1.0" "phpstan/phpstan-strict-rules": "^1.0"
}, },
"type": "library", "type": "library",
@ -653,32 +653,32 @@
"description": "A RakNet server implementation written in PHP", "description": "A RakNet server implementation written in PHP",
"support": { "support": {
"issues": "https://github.com/pmmp/RakLib/issues", "issues": "https://github.com/pmmp/RakLib/issues",
"source": "https://github.com/pmmp/RakLib/tree/0.15.1" "source": "https://github.com/pmmp/RakLib/tree/1.1.1"
}, },
"time": "2023-03-07T15:10:34+00:00" "time": "2024-03-04T14:02:14+00:00"
}, },
{ {
"name": "pocketmine/raklib-ipc", "name": "pocketmine/raklib-ipc",
"version": "0.2.0", "version": "1.0.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/pmmp/RakLibIpc.git", "url": "https://github.com/pmmp/RakLibIpc.git",
"reference": "26ed56fa9db06e4ca6e8920c0ede2e01e219bb9c" "reference": "ce632ef2c6743e71eddb5dc329c49af6555f90bc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/pmmp/RakLibIpc/zipball/26ed56fa9db06e4ca6e8920c0ede2e01e219bb9c", "url": "https://api.github.com/repos/pmmp/RakLibIpc/zipball/ce632ef2c6743e71eddb5dc329c49af6555f90bc",
"reference": "26ed56fa9db06e4ca6e8920c0ede2e01e219bb9c", "reference": "ce632ef2c6743e71eddb5dc329c49af6555f90bc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^8.0", "php": "^8.0",
"php-64bit": "*", "php-64bit": "*",
"pocketmine/binaryutils": "^0.2.0", "pocketmine/binaryutils": "^0.2.0",
"pocketmine/raklib": "^0.15.0" "pocketmine/raklib": "^0.15.0 || ^1.0.0"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "1.9.17", "phpstan/phpstan": "1.10.1",
"phpstan/phpstan-strict-rules": "^1.0.0" "phpstan/phpstan-strict-rules": "^1.0.0"
}, },
"type": "library", "type": "library",
@ -694,9 +694,9 @@
"description": "Channel-based protocols for inter-thread/inter-process communication with RakLib", "description": "Channel-based protocols for inter-thread/inter-process communication with RakLib",
"support": { "support": {
"issues": "https://github.com/pmmp/RakLibIpc/issues", "issues": "https://github.com/pmmp/RakLibIpc/issues",
"source": "https://github.com/pmmp/RakLibIpc/tree/0.2.0" "source": "https://github.com/pmmp/RakLibIpc/tree/1.0.1"
}, },
"time": "2023-02-13T13:40:40+00:00" "time": "2024-03-01T15:55:05+00:00"
}, },
{ {
"name": "pocketmine/snooze", "name": "pocketmine/snooze",

View File

@ -101,6 +101,8 @@ use pocketmine\player\Player;
use pocketmine\player\PlayerInfo; use pocketmine\player\PlayerInfo;
use pocketmine\player\UsedChunkStatus; use pocketmine\player\UsedChunkStatus;
use pocketmine\player\XboxLivePlayerInfo; use pocketmine\player\XboxLivePlayerInfo;
use pocketmine\promise\Promise;
use pocketmine\promise\PromiseResolver;
use pocketmine\Server; use pocketmine\Server;
use pocketmine\timings\Timings; use pocketmine\timings\Timings;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
@ -159,15 +161,24 @@ class NetworkSession{
/** @var string[] */ /** @var string[] */
private array $sendBuffer = []; private array $sendBuffer = [];
/** /**
* @var \SplQueue|CompressBatchPromise[]|string[] * @var PromiseResolver[]
* @phpstan-var \SplQueue<CompressBatchPromise|string> * @phpstan-var list<PromiseResolver<true>>
*/ */
private array $sendBufferAckPromises = [];
/** @phpstan-var \SplQueue<array{CompressBatchPromise|string, list<PromiseResolver<true>>}> */
private \SplQueue $compressedQueue; private \SplQueue $compressedQueue;
private bool $forceAsyncCompression = true; private bool $forceAsyncCompression = true;
private bool $enableCompression = false; //disabled until handshake completed private bool $enableCompression = false; //disabled until handshake completed
private int $nextAckReceiptId = 0;
/**
* @var PromiseResolver[][]
* @phpstan-var array<int, list<PromiseResolver<true>>>
*/
private array $ackPromisesByReceiptId = [];
private ?InventoryManager $invManager = null; private ?InventoryManager $invManager = null;
/** /**
@ -465,7 +476,23 @@ class NetworkSession{
} }
} }
public function sendDataPacket(ClientboundPacket $packet, bool $immediate = false) : bool{ public function handleAckReceipt(int $receiptId) : void{
if(!$this->connected){
return;
}
if(isset($this->ackPromisesByReceiptId[$receiptId])){
$promises = $this->ackPromisesByReceiptId[$receiptId];
unset($this->ackPromisesByReceiptId[$receiptId]);
foreach($promises as $promise){
$promise->resolve(true);
}
}
}
/**
* @phpstan-param PromiseResolver<true>|null $ackReceiptResolver
*/
private function sendDataPacketInternal(ClientboundPacket $packet, bool $immediate, ?PromiseResolver $ackReceiptResolver) : bool{
if(!$this->connected){ if(!$this->connected){
return false; return false;
} }
@ -488,6 +515,9 @@ class NetworkSession{
$packets = [$packet]; $packets = [$packet];
} }
if($ackReceiptResolver !== null){
$this->sendBufferAckPromises[] = $ackReceiptResolver;
}
foreach($packets as $evPacket){ foreach($packets as $evPacket){
$this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder(), $evPacket)); $this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder(), $evPacket));
} }
@ -501,6 +531,23 @@ class NetworkSession{
} }
} }
public function sendDataPacket(ClientboundPacket $packet, bool $immediate = false) : bool{
return $this->sendDataPacketInternal($packet, $immediate, null);
}
/**
* @phpstan-return Promise<true>
*/
public function sendDataPacketWithReceipt(ClientboundPacket $packet, bool $immediate = false) : Promise{
$resolver = new PromiseResolver();
if(!$this->sendDataPacketInternal($packet, $immediate, $resolver)){
$resolver->reject();
}
return $resolver->getPromise();
}
/** /**
* @internal * @internal
*/ */
@ -542,7 +589,9 @@ class NetworkSession{
$batch = $stream->getBuffer(); $batch = $stream->getBuffer();
} }
$this->sendBuffer = []; $this->sendBuffer = [];
$this->queueCompressedNoBufferFlush($batch, $immediate); $ackPromises = $this->sendBufferAckPromises;
$this->sendBufferAckPromises = [];
$this->queueCompressedNoBufferFlush($batch, $immediate, $ackPromises);
}finally{ }finally{
Timings::$playerNetworkSend->stopTiming(); Timings::$playerNetworkSend->stopTiming();
} }
@ -569,22 +618,27 @@ class NetworkSession{
} }
} }
private function queueCompressedNoBufferFlush(CompressBatchPromise|string $batch, bool $immediate = false) : void{ /**
* @param PromiseResolver[] $ackPromises
*
* @phpstan-param list<PromiseResolver<true>> $ackPromises
*/
private function queueCompressedNoBufferFlush(CompressBatchPromise|string $batch, bool $immediate = false, array $ackPromises = []) : void{
Timings::$playerNetworkSend->startTiming(); Timings::$playerNetworkSend->startTiming();
try{ try{
if(is_string($batch)){ if(is_string($batch)){
if($immediate){ if($immediate){
//Skips all queues //Skips all queues
$this->sendEncoded($batch, true); $this->sendEncoded($batch, true, $ackPromises);
}else{ }else{
$this->compressedQueue->enqueue($batch); $this->compressedQueue->enqueue([$batch, $ackPromises]);
$this->flushCompressedQueue(); $this->flushCompressedQueue();
} }
}elseif($immediate){ }elseif($immediate){
//Skips all queues //Skips all queues
$this->sendEncoded($batch->getResult(), true); $this->sendEncoded($batch->getResult(), true, $ackPromises);
}else{ }else{
$this->compressedQueue->enqueue($batch); $this->compressedQueue->enqueue([$batch, $ackPromises]);
$batch->onResolve(function() : void{ $batch->onResolve(function() : void{
if($this->connected){ if($this->connected){
$this->flushCompressedQueue(); $this->flushCompressedQueue();
@ -601,14 +655,14 @@ class NetworkSession{
try{ try{
while(!$this->compressedQueue->isEmpty()){ while(!$this->compressedQueue->isEmpty()){
/** @var CompressBatchPromise|string $current */ /** @var CompressBatchPromise|string $current */
$current = $this->compressedQueue->bottom(); [$current, $ackPromises] = $this->compressedQueue->bottom();
if(is_string($current)){ if(is_string($current)){
$this->compressedQueue->dequeue(); $this->compressedQueue->dequeue();
$this->sendEncoded($current); $this->sendEncoded($current, false, $ackPromises);
}elseif($current->hasResult()){ }elseif($current->hasResult()){
$this->compressedQueue->dequeue(); $this->compressedQueue->dequeue();
$this->sendEncoded($current->getResult()); $this->sendEncoded($current->getResult(), false, $ackPromises);
}else{ }else{
//can't send any more queued until this one is ready //can't send any more queued until this one is ready
@ -620,13 +674,24 @@ class NetworkSession{
} }
} }
private function sendEncoded(string $payload, bool $immediate = false) : void{ /**
* @param PromiseResolver[] $ackPromises
* @phpstan-param list<PromiseResolver<true>> $ackPromises
*/
private function sendEncoded(string $payload, bool $immediate, array $ackPromises) : void{
if($this->cipher !== null){ if($this->cipher !== null){
Timings::$playerNetworkSendEncrypt->startTiming(); Timings::$playerNetworkSendEncrypt->startTiming();
$payload = $this->cipher->encrypt($payload); $payload = $this->cipher->encrypt($payload);
Timings::$playerNetworkSendEncrypt->stopTiming(); Timings::$playerNetworkSendEncrypt->stopTiming();
} }
$this->sender->send($payload, $immediate);
if(count($ackPromises) > 0){
$ackReceiptId = $this->nextAckReceiptId++;
$this->ackPromisesByReceiptId[$ackReceiptId] = $ackPromises;
}else{
$ackReceiptId = null;
}
$this->sender->send($payload, $immediate, $ackReceiptId);
} }
/** /**
@ -646,6 +711,19 @@ class NetworkSession{
$this->setHandler(null); $this->setHandler(null);
$this->connected = false; $this->connected = false;
$ackPromisesByReceiptId = $this->ackPromisesByReceiptId;
$this->ackPromisesByReceiptId = [];
foreach($ackPromisesByReceiptId as $resolvers){
foreach($resolvers as $resolver){
$resolver->reject();
}
}
$sendBufferAckPromises = $this->sendBufferAckPromises;
$this->sendBufferAckPromises = [];
foreach($sendBufferAckPromises as $resolver){
$resolver->reject();
}
$this->logger->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_close($reason))); $this->logger->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_close($reason)));
} }
} }

View File

@ -28,7 +28,7 @@ interface PacketSender{
/** /**
* Pushes a packet into the channel to be processed. * Pushes a packet into the channel to be processed.
*/ */
public function send(string $payload, bool $immediate) : void; public function send(string $payload, bool $immediate, ?int $receiptId) : void;
/** /**
* Closes the channel, terminating the connection. * Closes the channel, terminating the connection.

View File

@ -51,7 +51,13 @@ use function substr;
* packs to the client. * packs to the client.
*/ */
class ResourcePacksPacketHandler extends PacketHandler{ class ResourcePacksPacketHandler extends PacketHandler{
private const PACK_CHUNK_SIZE = 128 * 1024; //128KB private const PACK_CHUNK_SIZE = 256 * 1024; //256KB
/**
* Larger values allow downloading more chunks at the same time, increasing download speed, but the client may choke
* and cause the download speed to drop (due to ACKs taking too long to arrive).
*/
private const MAX_CONCURRENT_CHUNK_REQUESTS = 1;
/** /**
* @var ResourcePack[] * @var ResourcePack[]
@ -62,6 +68,11 @@ class ResourcePacksPacketHandler extends PacketHandler{
/** @var bool[][] uuid => [chunk index => hasSent] */ /** @var bool[][] uuid => [chunk index => hasSent] */
private array $downloadedChunks = []; private array $downloadedChunks = [];
/** @phpstan-var \SplQueue<array{ResourcePack, int}> */
private \SplQueue $requestQueue;
private int $activeRequests = 0;
/** /**
* @param ResourcePack[] $resourcePackStack * @param ResourcePack[] $resourcePackStack
* @param string[] $encryptionKeys pack UUID => key, leave unset for any packs that are not encrypted * @param string[] $encryptionKeys pack UUID => key, leave unset for any packs that are not encrypted
@ -77,6 +88,7 @@ class ResourcePacksPacketHandler extends PacketHandler{
private bool $mustAccept, private bool $mustAccept,
private \Closure $completionCallback private \Closure $completionCallback
){ ){
$this->requestQueue = new \SplQueue();
foreach($resourcePackStack as $pack){ foreach($resourcePackStack as $pack){
$this->resourcePacksById[$pack->getPackId()] = $pack; $this->resourcePacksById[$pack->getPackId()] = $pack;
} }
@ -197,8 +209,37 @@ class ResourcePacksPacketHandler extends PacketHandler{
$this->downloadedChunks[$packId][$packet->chunkIndex] = true; $this->downloadedChunks[$packId][$packet->chunkIndex] = true;
} }
$this->session->sendDataPacket(ResourcePackChunkDataPacket::create($packId, $packet->chunkIndex, $offset, $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE))); $this->requestQueue->enqueue([$pack, $packet->chunkIndex]);
$this->processChunkRequestQueue();
return true; return true;
} }
private function processChunkRequestQueue() : void{
if($this->activeRequests >= self::MAX_CONCURRENT_CHUNK_REQUESTS || $this->requestQueue->isEmpty()){
return;
}
/**
* @var ResourcePack $pack
* @var int $chunkIndex
*/
[$pack, $chunkIndex] = $this->requestQueue->dequeue();
$packId = $pack->getPackId();
$offset = $chunkIndex * self::PACK_CHUNK_SIZE;
$chunkData = $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE);
$this->activeRequests++;
$this->session
->sendDataPacketWithReceipt(ResourcePackChunkDataPacket::create($packId, $chunkIndex, $offset, $chunkData))
->onCompletion(
function() : void{
$this->activeRequests--;
$this->processChunkRequestQueue();
},
function() : void{
//this may have been rejected because of a disconnection - this will do nothing in that case
$this->disconnectWithError("Plugin interrupted sending of resource packs");
}
);
}
} }

View File

@ -252,7 +252,9 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
} }
public function onPacketAck(int $sessionId, int $identifierACK) : void{ public function onPacketAck(int $sessionId, int $identifierACK) : void{
if(isset($this->sessions[$sessionId])){
$this->sessions[$sessionId]->handleAckReceipt($identifierACK);
}
} }
public function setName(string $name) : void{ public function setName(string $name) : void{
@ -289,12 +291,13 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
$this->network->getBandwidthTracker()->add($bytesSentDiff, $bytesReceivedDiff); $this->network->getBandwidthTracker()->add($bytesSentDiff, $bytesReceivedDiff);
} }
public function putPacket(int $sessionId, string $payload, bool $immediate = true) : void{ public function putPacket(int $sessionId, string $payload, bool $immediate = true, ?int $receiptId = null) : void{
if(isset($this->sessions[$sessionId])){ if(isset($this->sessions[$sessionId])){
$pk = new EncapsulatedPacket(); $pk = new EncapsulatedPacket();
$pk->buffer = self::MCPE_RAKNET_PACKET_ID . $payload; $pk->buffer = self::MCPE_RAKNET_PACKET_ID . $payload;
$pk->reliability = PacketReliability::RELIABLE_ORDERED; $pk->reliability = PacketReliability::RELIABLE_ORDERED;
$pk->orderChannel = 0; $pk->orderChannel = 0;
$pk->identifierACK = $receiptId;
$this->interface->sendEncapsulated($sessionId, $pk, $immediate); $this->interface->sendEncapsulated($sessionId, $pk, $immediate);
} }

View File

@ -33,9 +33,9 @@ class RakLibPacketSender implements PacketSender{
private RakLibInterface $handler private RakLibInterface $handler
){} ){}
public function send(string $payload, bool $immediate) : void{ public function send(string $payload, bool $immediate, ?int $receiptId) : void{
if(!$this->closed){ if(!$this->closed){
$this->handler->putPacket($this->sessionId, $payload, $immediate); $this->handler->putPacket($this->sessionId, $payload, $immediate, $receiptId);
} }
} }