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:
""
125 $this->session->getLogger()->debug(
"Waiting for client to accept resource packs");
128 private function disconnectWithError(
string $error) : void{
129 $this->session->disconnectWithError(
130 reason:
"Error downloading resource packs: " . $error,
135 public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
136 switch($packet->status){
137 case ResourcePackClientResponsePacket::STATUS_REFUSED:
139 $this->session->disconnect(
"Refused resource packs",
"You must accept resource packs to join this server.",
true);
141 case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
142 foreach($packet->packIds as $uuid){
144 $splitPos = strpos($uuid,
"_");
145 if($splitPos !== false){
146 $uuid = substr($uuid, 0, $splitPos);
148 $pack = $this->getPackById($uuid);
150 if(!($pack instanceof ResourcePack)){
152 $this->disconnectWithError(
"Unknown pack $uuid requested, available packs: " . implode(
", ", array_keys($this->resourcePacksById)));
156 $this->session->sendDataPacket(ResourcePackDataInfoPacket::create(
158 self::PACK_CHUNK_SIZE,
159 (
int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE),
160 $pack->getPackSize(),
163 ResourcePackType::RESOURCES
166 $this->session->getLogger()->debug(
"Player requested download of " . count($packet->packIds) .
" resource packs");
169 case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS:
170 $stack = array_map(
static function(ResourcePack $pack) : ResourcePackStackEntry{
171 return new ResourcePackStackEntry($pack->getPackId(), $pack->getPackVersion(),
"");
172 }, $this->resourcePackStack);
175 $stack[] =
new ResourcePackStackEntry(
"0fba4063-dba1-4281-9b89-ff9390653530",
"1.0.0",
"");
180 $this->session->sendDataPacket(ResourcePackStackPacket::create($stack, [],
false, ProtocolInfo::MINECRAFT_VERSION_NETWORK,
new Experiments([],
false),
false));
181 $this->session->getLogger()->debug(
"Applying resource pack stack");
183 case ResourcePackClientResponsePacket::STATUS_COMPLETED:
184 $this->session->getLogger()->debug(
"Resource packs sequence completed");
185 ($this->completionCallback)();
194 public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{
195 $pack = $this->getPackById($packet->packId);
196 if(!($pack instanceof ResourcePack)){
197 $this->disconnectWithError(
"Invalid request for chunk $packet->chunkIndex of unknown pack $packet->packId, available packs: " . implode(
", ", array_keys($this->resourcePacksById)));
201 $packId = $pack->getPackId();
203 if(isset($this->downloadedChunks[$packId][$packet->chunkIndex])){
204 $this->disconnectWithError(
"Duplicate request for chunk $packet->chunkIndex of pack $packet->packId");
208 $offset = $packet->chunkIndex * self::PACK_CHUNK_SIZE;
209 if($offset < 0 || $offset >= $pack->getPackSize()){
210 $this->disconnectWithError(
"Invalid out-of-bounds request for chunk $packet->chunkIndex of $packet->packId: offset $offset, file size " . $pack->getPackSize());
214 if(!isset($this->downloadedChunks[$packId])){
215 $this->downloadedChunks[$packId] = [$packet->chunkIndex =>
true];
217 $this->downloadedChunks[$packId][$packet->chunkIndex] =
true;
220 $this->requestQueue->enqueue([$pack, $packet->chunkIndex]);
221 $this->processChunkRequestQueue();
226 private function processChunkRequestQueue() : void{
227 if($this->activeRequests >= self::MAX_CONCURRENT_CHUNK_REQUESTS || $this->requestQueue->isEmpty()){
234 [$pack, $chunkIndex] = $this->requestQueue->dequeue();
236 $packId = $pack->getPackId();
237 $offset = $chunkIndex * self::PACK_CHUNK_SIZE;
238 $chunkData = $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE);
239 $this->activeRequests++;
241 ->sendDataPacketWithReceipt(ResourcePackChunkDataPacket::create($packId, $chunkIndex, $offset, $chunkData))
244 $this->activeRequests--;
245 $this->processChunkRequestQueue();
249 $this->disconnectWithError(
"Plugin interrupted sending of resource packs");