55 private const PACK_CHUNK_SIZE = 256 * 1024;
61 private const MAX_CONCURRENT_CHUNK_REQUESTS = 1;
67 private array $resourcePacksById = [];
70 private array $downloadedChunks = [];
73 private \SplQueue $requestQueue;
75 private int $activeRequests = 0;
87 private array $resourcePackStack,
88 private array $encryptionKeys,
89 private bool $mustAccept,
90 private \Closure $completionCallback
92 $this->requestQueue = new \SplQueue();
93 foreach($resourcePackStack as $pack){
94 $this->resourcePacksById[$pack->getPackId()] = $pack;
98 private function getPackById(
string $id) : ?
ResourcePack{
99 return $this->resourcePacksById[strtolower($id)] ?? null;
102 public function setUp() : void{
103 $resourcePackEntries = array_map(function(ResourcePack $pack) : ResourcePackInfoEntry{
106 return new ResourcePackInfoEntry(
107 Uuid::fromString($pack->getPackId()),
108 $pack->getPackVersion(),
109 $pack->getPackSize(),
110 $this->encryptionKeys[$pack->getPackId()] ??
"",
115 }, $this->resourcePackStack);
117 $this->session->sendDataPacket(ResourcePacksInfoPacket::create(
118 resourcePackEntries: $resourcePackEntries,
119 mustAccept: $this->mustAccept,
122 worldTemplateId: Uuid::fromString(Uuid::NIL),
123 worldTemplateVersion:
"",
124 forceDisableVibrantVisuals:
true,
126 $this->session->getLogger()->debug(
"Waiting for client to accept resource packs");
129 private function disconnectWithError(
string $error) : void{
130 $this->session->disconnectWithError(
131 reason:
"Error downloading resource packs: " . $error,
136 public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
137 switch($packet->status){
138 case ResourcePackClientResponsePacket::STATUS_REFUSED:
140 $this->session->disconnect(
"Refused resource packs",
"You must accept resource packs to join this server.",
true);
142 case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
143 foreach($packet->packIds as $uuid){
145 $splitPos = strpos($uuid,
"_");
146 if($splitPos !== false){
147 $uuid = substr($uuid, 0, $splitPos);
149 $pack = $this->getPackById($uuid);
151 if(!($pack instanceof ResourcePack)){
153 $this->disconnectWithError(
"Unknown pack $uuid requested, available packs: " . implode(
", ", array_keys($this->resourcePacksById)));
157 $this->session->sendDataPacket(ResourcePackDataInfoPacket::create(
159 self::PACK_CHUNK_SIZE,
160 (
int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE),
161 $pack->getPackSize(),
164 ResourcePackType::RESOURCES
167 $this->session->getLogger()->debug(
"Player requested download of " . count($packet->packIds) .
" resource packs");
170 case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS:
171 $stack = array_map(
static function(ResourcePack $pack) : ResourcePackStackEntry{
172 return new ResourcePackStackEntry($pack->getPackId(), $pack->getPackVersion(),
"");
173 }, $this->resourcePackStack);
176 $stack[] =
new ResourcePackStackEntry(
"0fba4063-dba1-4281-9b89-ff9390653530",
"1.0.0",
"");
181 $this->session->sendDataPacket(ResourcePackStackPacket::create($stack, [],
false, ProtocolInfo::MINECRAFT_VERSION_NETWORK,
new Experiments([],
false),
false));
182 $this->session->getLogger()->debug(
"Applying resource pack stack");
184 case ResourcePackClientResponsePacket::STATUS_COMPLETED:
185 $this->session->getLogger()->debug(
"Resource packs sequence completed");
186 ($this->completionCallback)();
195 public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{
196 $pack = $this->getPackById($packet->packId);
197 if(!($pack instanceof ResourcePack)){
198 $this->disconnectWithError(
"Invalid request for chunk $packet->chunkIndex of unknown pack $packet->packId, available packs: " . implode(
", ", array_keys($this->resourcePacksById)));
202 $packId = $pack->getPackId();
204 if(isset($this->downloadedChunks[$packId][$packet->chunkIndex])){
205 $this->disconnectWithError(
"Duplicate request for chunk $packet->chunkIndex of pack $packet->packId");
209 $offset = $packet->chunkIndex * self::PACK_CHUNK_SIZE;
210 if($offset < 0 || $offset >= $pack->getPackSize()){
211 $this->disconnectWithError(
"Invalid out-of-bounds request for chunk $packet->chunkIndex of $packet->packId: offset $offset, file size " . $pack->getPackSize());
215 if(!isset($this->downloadedChunks[$packId])){
216 $this->downloadedChunks[$packId] = [$packet->chunkIndex =>
true];
218 $this->downloadedChunks[$packId][$packet->chunkIndex] =
true;
221 $this->requestQueue->enqueue([$pack, $packet->chunkIndex]);
222 $this->processChunkRequestQueue();
227 private function processChunkRequestQueue() : void{
228 if($this->activeRequests >= self::MAX_CONCURRENT_CHUNK_REQUESTS || $this->requestQueue->isEmpty()){
235 [$pack, $chunkIndex] = $this->requestQueue->dequeue();
237 $packId = $pack->getPackId();
238 $offset = $chunkIndex * self::PACK_CHUNK_SIZE;
239 $chunkData = $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE);
240 $this->activeRequests++;
242 ->sendDataPacketWithReceipt(ResourcePackChunkDataPacket::create($packId, $chunkIndex, $offset, $chunkData))
245 $this->activeRequests--;
246 $this->processChunkRequestQueue();
250 $this->disconnectWithError(
"Plugin interrupted sending of resource packs");