54 private const PACK_CHUNK_SIZE = 256 * 1024;
60 private const MAX_CONCURRENT_CHUNK_REQUESTS = 1;
66 private array $resourcePacksById = [];
69 private array $downloadedChunks = [];
72 private \SplQueue $requestQueue;
74 private int $activeRequests = 0;
86 private array $resourcePackStack,
87 private array $encryptionKeys,
88 private bool $mustAccept,
89 private \Closure $completionCallback
91 $this->requestQueue = new \SplQueue();
92 foreach($resourcePackStack as $pack){
93 $this->resourcePacksById[$pack->getPackId()] = $pack;
97 private function getPackById(
string $id) : ?
ResourcePack{
98 return $this->resourcePacksById[strtolower($id)] ?? null;
101 public function setUp() : void{
102 $resourcePackEntries = array_map(function(ResourcePack $pack) : ResourcePackInfoEntry{
105 return new ResourcePackInfoEntry(
107 $pack->getPackVersion(),
108 $pack->getPackSize(),
109 $this->encryptionKeys[$pack->getPackId()] ??
"",
114 }, $this->resourcePackStack);
116 $this->session->sendDataPacket(ResourcePacksInfoPacket::create(
117 resourcePackEntries: $resourcePackEntries,
118 mustAccept: $this->mustAccept,
122 $this->session->getLogger()->debug(
"Waiting for client to accept resource packs");
125 private function disconnectWithError(
string $error) : void{
126 $this->session->disconnectWithError(
127 reason:
"Error downloading resource packs: " . $error,
132 public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
133 switch($packet->status){
134 case ResourcePackClientResponsePacket::STATUS_REFUSED:
136 $this->session->disconnect(
"Refused resource packs",
"You must accept resource packs to join this server.",
true);
138 case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
139 foreach($packet->packIds as $uuid){
141 $splitPos = strpos($uuid,
"_");
142 if($splitPos !== false){
143 $uuid = substr($uuid, 0, $splitPos);
145 $pack = $this->getPackById($uuid);
147 if(!($pack instanceof ResourcePack)){
149 $this->disconnectWithError(
"Unknown pack $uuid requested, available packs: " . implode(
", ", array_keys($this->resourcePacksById)));
153 $this->session->sendDataPacket(ResourcePackDataInfoPacket::create(
155 self::PACK_CHUNK_SIZE,
156 (
int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE),
157 $pack->getPackSize(),
160 ResourcePackType::RESOURCES
163 $this->session->getLogger()->debug(
"Player requested download of " . count($packet->packIds) .
" resource packs");
166 case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS:
167 $stack = array_map(
static function(ResourcePack $pack) : ResourcePackStackEntry{
168 return new ResourcePackStackEntry($pack->getPackId(), $pack->getPackVersion(),
"");
169 }, $this->resourcePackStack);
172 $stack[] =
new ResourcePackStackEntry(
"0fba4063-dba1-4281-9b89-ff9390653530",
"1.0.0",
"");
177 $this->session->sendDataPacket(ResourcePackStackPacket::create($stack, [],
false, ProtocolInfo::MINECRAFT_VERSION_NETWORK,
new Experiments([],
false),
false));
178 $this->session->getLogger()->debug(
"Applying resource pack stack");
180 case ResourcePackClientResponsePacket::STATUS_COMPLETED:
181 $this->session->getLogger()->debug(
"Resource packs sequence completed");
182 ($this->completionCallback)();
191 public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{
192 $pack = $this->getPackById($packet->packId);
193 if(!($pack instanceof ResourcePack)){
194 $this->disconnectWithError(
"Invalid request for chunk $packet->chunkIndex of unknown pack $packet->packId, available packs: " . implode(
", ", array_keys($this->resourcePacksById)));
198 $packId = $pack->getPackId();
200 if(isset($this->downloadedChunks[$packId][$packet->chunkIndex])){
201 $this->disconnectWithError(
"Duplicate request for chunk $packet->chunkIndex of pack $packet->packId");
205 $offset = $packet->chunkIndex * self::PACK_CHUNK_SIZE;
206 if($offset < 0 || $offset >= $pack->getPackSize()){
207 $this->disconnectWithError(
"Invalid out-of-bounds request for chunk $packet->chunkIndex of $packet->packId: offset $offset, file size " . $pack->getPackSize());
211 if(!isset($this->downloadedChunks[$packId])){
212 $this->downloadedChunks[$packId] = [$packet->chunkIndex =>
true];
214 $this->downloadedChunks[$packId][$packet->chunkIndex] =
true;
217 $this->requestQueue->enqueue([$pack, $packet->chunkIndex]);
218 $this->processChunkRequestQueue();
223 private function processChunkRequestQueue() : void{
224 if($this->activeRequests >= self::MAX_CONCURRENT_CHUNK_REQUESTS || $this->requestQueue->isEmpty()){
231 [$pack, $chunkIndex] = $this->requestQueue->dequeue();
233 $packId = $pack->getPackId();
234 $offset = $chunkIndex * self::PACK_CHUNK_SIZE;
235 $chunkData = $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE);
236 $this->activeRequests++;
238 ->sendDataPacketWithReceipt(ResourcePackChunkDataPacket::create($packId, $chunkIndex, $offset, $chunkData))
241 $this->activeRequests--;
242 $this->processChunkRequestQueue();
246 $this->disconnectWithError(
"Plugin interrupted sending of resource packs");