57    private const PACK_CHUNK_SIZE = 256 * 1024; 
 
   63    private const MAX_CONCURRENT_CHUNK_REQUESTS = 1;
 
   69    private const CHEMISTRY_RESOURCE_PACKS = [
 
   70        [
"b41c2785-c512-4a49-af56-3a87afd47c57", 
"1.21.30"],
 
   71        [
"a4df0cb3-17be-4163-88d7-fcf7002b935d", 
"1.21.20"],
 
   72        [
"d19adffe-a2e1-4b02-8436-ca4583368c89", 
"1.21.10"],
 
   73        [
"85d5603d-2824-4b21-8044-34f441f4fce1", 
"1.21.0"],
 
   74        [
"e977cd13-0a11-4618-96fb-03dfe9c43608", 
"1.20.60"],
 
   75        [
"0674721c-a0aa-41a1-9ba8-1ed33ea3e7ed", 
"1.20.50"],
 
   76        [
"0fba4063-dba1-4281-9b89-ff9390653530", 
"1.0.0"],
 
   83    private array $resourcePacksById = [];
 
   85    private bool $requestedMetadata = 
false;
 
   86    private bool $requestedStack = 
false;
 
   89    private array $downloadedChunks = [];
 
   92    private \SplQueue $requestQueue;
 
   94    private int $activeRequests = 0;
 
  106        private array $resourcePackStack,
 
  107        private array $encryptionKeys,
 
  108        private bool $mustAccept,
 
  109        private \Closure $completionCallback
 
  111        $this->requestQueue = new \SplQueue();
 
  112        foreach($resourcePackStack as $pack){
 
  113            $this->resourcePacksById[$pack->getPackId()] = $pack;
 
 
  117    private function getPackById(
string $id) : ?
ResourcePack{
 
  118        return $this->resourcePacksById[strtolower($id)] ?? null;
 
  121    public function setUp() : void{
 
  122        $resourcePackEntries = array_map(function(ResourcePack $pack) : ResourcePackInfoEntry{
 
  125            return new ResourcePackInfoEntry(
 
  126                Uuid::fromString($pack->getPackId()),
 
  127                $pack->getPackVersion(),
 
  128                $pack->getPackSize(),
 
  129                $this->encryptionKeys[$pack->getPackId()] ?? 
"",
 
  134        }, $this->resourcePackStack);
 
  136        $this->session->sendDataPacket(ResourcePacksInfoPacket::create(
 
  137            resourcePackEntries: $resourcePackEntries,
 
  138            mustAccept: $this->mustAccept,
 
  141            worldTemplateId: Uuid::fromString(Uuid::NIL),
 
  142            worldTemplateVersion: 
"",
 
  143            forceDisableVibrantVisuals: 
true,
 
  145        $this->session->getLogger()->debug(
"Waiting for client to accept resource packs");
 
  148    private function disconnectWithError(
string $error) : void{
 
  149        $this->session->disconnectWithError(
 
  150            reason: 
"Error downloading resource packs: " . $error,
 
  155    public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
 
  156        switch($packet->status){
 
  157            case ResourcePackClientResponsePacket::STATUS_REFUSED:
 
  159                $this->session->disconnect(
"Refused resource packs", 
"You must accept resource packs to join this server.", 
true);
 
  161            case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
 
  162                if($this->requestedMetadata){
 
  163                    throw new PacketHandlingException(
"Cannot request resource pack metadata multiple times");
 
  165                $this->requestedMetadata = 
true;
 
  167                if($this->requestedStack){
 
  169                    throw new PacketHandlingException(
"Cannot request resource pack metadata after resource pack stack");
 
  172                if(count($packet->packIds) > count($this->resourcePacksById)){
 
  173                    throw new PacketHandlingException(sprintf(
"Requested metadata for more resource packs (%d) than available on the server (%d)", count($packet->packIds), count($this->resourcePacksById)));
 
  177                foreach($packet->packIds as $uuid){
 
  179                    $splitPos = strpos($uuid, 
"_");
 
  180                    if($splitPos !== false){
 
  181                        $uuid = substr($uuid, 0, $splitPos);
 
  183                    $pack = $this->getPackById($uuid);
 
  185                    if(!($pack instanceof ResourcePack)){
 
  187                        $this->disconnectWithError(
"Unknown pack $uuid requested, available packs: " . implode(
", ", array_keys($this->resourcePacksById)));
 
  190                    if(isset($seen[$pack->getPackId()])){
 
  191                        throw new PacketHandlingException(
"Repeated metadata request for pack $uuid");
 
  194                    $this->session->sendDataPacket(ResourcePackDataInfoPacket::create(
 
  196                        self::PACK_CHUNK_SIZE,
 
  197                        (
int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE),
 
  198                        $pack->getPackSize(),
 
  201                        ResourcePackType::RESOURCES 
 
  203                    $seen[$pack->getPackId()] = 
true;
 
  205                $this->session->getLogger()->debug(
"Player requested download of " . count($packet->packIds) . 
" resource packs");
 
  207            case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS:
 
  208                if($this->requestedStack){
 
  209                    throw new PacketHandlingException(
"Cannot request resource pack stack multiple times");
 
  211                $this->requestedStack = 
true;
 
  213                $stack = array_map(
static function(ResourcePack $pack) : ResourcePackStackEntry{
 
  214                    return new ResourcePackStackEntry($pack->getPackId(), $pack->getPackVersion(), 
""); 
 
  215                }, $this->resourcePackStack);
 
  218                foreach(self::CHEMISTRY_RESOURCE_PACKS as [$uuid, $version]){
 
  219                    $stack[] = 
new ResourcePackStackEntry($uuid, $version, 
"");
 
  225                $this->session->sendDataPacket(ResourcePackStackPacket::create($stack, [], 
false, ProtocolInfo::MINECRAFT_VERSION_NETWORK, 
new Experiments([], 
false), 
false));
 
  226                $this->session->getLogger()->debug(
"Applying resource pack stack");
 
  228            case ResourcePackClientResponsePacket::STATUS_COMPLETED:
 
  229                $this->session->getLogger()->debug(
"Resource packs sequence completed");
 
  230                ($this->completionCallback)();
 
  239    public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{
 
  240        $pack = $this->getPackById($packet->packId);
 
  241        if(!($pack instanceof ResourcePack)){
 
  242            $this->disconnectWithError(
"Invalid request for chunk $packet->chunkIndex of unknown pack $packet->packId, available packs: " . implode(
", ", array_keys($this->resourcePacksById)));
 
  246        $packId = $pack->getPackId(); 
 
  248        if(isset($this->downloadedChunks[$packId][$packet->chunkIndex])){
 
  249            $this->disconnectWithError(
"Duplicate request for chunk $packet->chunkIndex of pack $packet->packId");
 
  253        $offset = $packet->chunkIndex * self::PACK_CHUNK_SIZE;
 
  254        if($offset < 0 || $offset >= $pack->getPackSize()){
 
  255            $this->disconnectWithError(
"Invalid out-of-bounds request for chunk $packet->chunkIndex of $packet->packId: offset $offset, file size " . $pack->getPackSize());
 
  259        if(!isset($this->downloadedChunks[$packId])){
 
  260            $this->downloadedChunks[$packId] = [$packet->chunkIndex => 
true];
 
  262            $this->downloadedChunks[$packId][$packet->chunkIndex] = 
true;
 
  265        $this->requestQueue->enqueue([$pack, $packet->chunkIndex]);
 
  266        $this->processChunkRequestQueue();
 
  271    private function processChunkRequestQueue() : void{
 
  272        if($this->activeRequests >= self::MAX_CONCURRENT_CHUNK_REQUESTS || $this->requestQueue->isEmpty()){
 
  279        [$pack, $chunkIndex] = $this->requestQueue->dequeue();
 
  281        $packId = $pack->getPackId();
 
  282        $offset = $chunkIndex * self::PACK_CHUNK_SIZE;
 
  283        $chunkData = $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE);
 
  284        $this->activeRequests++;
 
  286            ->sendDataPacketWithReceipt(ResourcePackChunkDataPacket::create($packId, $chunkIndex, $offset, $chunkData))
 
  289                    $this->activeRequests--;
 
  290                    $this->processChunkRequestQueue();
 
  294                    $this->disconnectWithError(
"Plugin interrupted sending of resource packs");