57 private const PACK_CHUNK_SIZE = 256 * 1024;
63 private const MAX_CONCURRENT_CHUNK_REQUESTS = 1;
69 private array $resourcePacksById = [];
71 private bool $requestedMetadata =
false;
72 private bool $requestedStack =
false;
75 private array $downloadedChunks = [];
78 private \SplQueue $requestQueue;
80 private int $activeRequests = 0;
92 private array $resourcePackStack,
93 private array $encryptionKeys,
94 private bool $mustAccept,
95 private \Closure $completionCallback
97 $this->requestQueue = new \SplQueue();
98 foreach($resourcePackStack as $pack){
99 $this->resourcePacksById[$pack->getPackId()] = $pack;
103 private function getPackById(
string $id) : ?
ResourcePack{
104 return $this->resourcePacksById[strtolower($id)] ?? null;
107 public function setUp() : void{
108 $resourcePackEntries = array_map(function(ResourcePack $pack) : ResourcePackInfoEntry{
111 return new ResourcePackInfoEntry(
112 Uuid::fromString($pack->getPackId()),
113 $pack->getPackVersion(),
114 $pack->getPackSize(),
115 $this->encryptionKeys[$pack->getPackId()] ??
"",
120 }, $this->resourcePackStack);
122 $this->session->sendDataPacket(ResourcePacksInfoPacket::create(
123 resourcePackEntries: $resourcePackEntries,
124 mustAccept: $this->mustAccept,
127 worldTemplateId: Uuid::fromString(Uuid::NIL),
128 worldTemplateVersion:
"",
129 forceDisableVibrantVisuals:
true,
131 $this->session->getLogger()->debug(
"Waiting for client to accept resource packs");
134 private function disconnectWithError(
string $error) : void{
135 $this->session->disconnectWithError(
136 reason:
"Error downloading resource packs: " . $error,
141 public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
142 switch($packet->status){
143 case ResourcePackClientResponsePacket::STATUS_REFUSED:
145 $this->session->disconnect(
"Refused resource packs",
"You must accept resource packs to join this server.",
true);
147 case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
148 if($this->requestedMetadata){
149 throw new PacketHandlingException(
"Cannot request resource pack metadata multiple times");
151 $this->requestedMetadata =
true;
153 if($this->requestedStack){
155 throw new PacketHandlingException(
"Cannot request resource pack metadata after resource pack stack");
158 if(count($packet->packIds) > count($this->resourcePacksById)){
159 throw new PacketHandlingException(sprintf(
"Requested metadata for more resource packs (%d) than available on the server (%d)", count($packet->packIds), count($this->resourcePacksById)));
163 foreach($packet->packIds as $uuid){
165 $splitPos = strpos($uuid,
"_");
166 if($splitPos !== false){
167 $uuid = substr($uuid, 0, $splitPos);
169 $pack = $this->getPackById($uuid);
171 if(!($pack instanceof ResourcePack)){
173 $this->disconnectWithError(
"Unknown pack $uuid requested, available packs: " . implode(
", ", array_keys($this->resourcePacksById)));
176 if(isset($seen[$pack->getPackId()])){
177 throw new PacketHandlingException(
"Repeated metadata request for pack $uuid");
180 $this->session->sendDataPacket(ResourcePackDataInfoPacket::create(
182 self::PACK_CHUNK_SIZE,
183 (
int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE),
184 $pack->getPackSize(),
187 ResourcePackType::RESOURCES
189 $seen[$pack->getPackId()] =
true;
191 $this->session->getLogger()->debug(
"Player requested download of " . count($packet->packIds) .
" resource packs");
193 case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS:
194 if($this->requestedStack){
195 throw new PacketHandlingException(
"Cannot request resource pack stack multiple times");
197 $this->requestedStack =
true;
199 $stack = array_map(
static function(ResourcePack $pack) : ResourcePackStackEntry{
200 return new ResourcePackStackEntry($pack->getPackId(), $pack->getPackVersion(),
"");
201 }, $this->resourcePackStack);
204 $stack[] =
new ResourcePackStackEntry(
"0fba4063-dba1-4281-9b89-ff9390653530",
"1.0.0",
"");
209 $this->session->sendDataPacket(ResourcePackStackPacket::create($stack, [],
false, ProtocolInfo::MINECRAFT_VERSION_NETWORK,
new Experiments([],
false),
false));
210 $this->session->getLogger()->debug(
"Applying resource pack stack");
212 case ResourcePackClientResponsePacket::STATUS_COMPLETED:
213 $this->session->getLogger()->debug(
"Resource packs sequence completed");
214 ($this->completionCallback)();
223 public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{
224 $pack = $this->getPackById($packet->packId);
225 if(!($pack instanceof ResourcePack)){
226 $this->disconnectWithError(
"Invalid request for chunk $packet->chunkIndex of unknown pack $packet->packId, available packs: " . implode(
", ", array_keys($this->resourcePacksById)));
230 $packId = $pack->getPackId();
232 if(isset($this->downloadedChunks[$packId][$packet->chunkIndex])){
233 $this->disconnectWithError(
"Duplicate request for chunk $packet->chunkIndex of pack $packet->packId");
237 $offset = $packet->chunkIndex * self::PACK_CHUNK_SIZE;
238 if($offset < 0 || $offset >= $pack->getPackSize()){
239 $this->disconnectWithError(
"Invalid out-of-bounds request for chunk $packet->chunkIndex of $packet->packId: offset $offset, file size " . $pack->getPackSize());
243 if(!isset($this->downloadedChunks[$packId])){
244 $this->downloadedChunks[$packId] = [$packet->chunkIndex =>
true];
246 $this->downloadedChunks[$packId][$packet->chunkIndex] =
true;
249 $this->requestQueue->enqueue([$pack, $packet->chunkIndex]);
250 $this->processChunkRequestQueue();
255 private function processChunkRequestQueue() : void{
256 if($this->activeRequests >= self::MAX_CONCURRENT_CHUNK_REQUESTS || $this->requestQueue->isEmpty()){
263 [$pack, $chunkIndex] = $this->requestQueue->dequeue();
265 $packId = $pack->getPackId();
266 $offset = $chunkIndex * self::PACK_CHUNK_SIZE;
267 $chunkData = $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE);
268 $this->activeRequests++;
270 ->sendDataPacketWithReceipt(ResourcePackChunkDataPacket::create($packId, $chunkIndex, $offset, $chunkData))
273 $this->activeRequests--;
274 $this->processChunkRequestQueue();
278 $this->disconnectWithError(
"Plugin interrupted sending of resource packs");