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 $resourcePacksByPrintableId = [];
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->resourcePacksByPrintableId[$pack->getPackId()->toString()] = $pack;
117 private function getPackByPrintableId(
string $printableId) : ?
ResourcePack{
118 return $this->resourcePacksByPrintableId[strtolower($printableId)] ?? null;
121 public function setUp() : void{
122 $resourcePackEntries = array_map(function(ResourcePack $pack) : ResourcePackInfoEntry{
125 $uuid = $pack->getPackId();
126 $printableId = $uuid->toString();
127 return new ResourcePackInfoEntry(
129 $pack->getPackVersion(),
130 $pack->getPackSize(),
131 $this->encryptionKeys[$printableId] ??
"",
136 }, $this->resourcePackStack);
138 $this->session->sendDataPacket(ResourcePacksInfoPacket::create(
139 resourcePackEntries: $resourcePackEntries,
140 mustAccept: $this->mustAccept,
143 worldTemplateId: Uuid::fromString(Uuid::NIL),
144 worldTemplateVersion:
"",
145 forceDisableVibrantVisuals:
true,
147 $this->session->getLogger()->debug(
"Waiting for client to accept resource packs");
150 private function disconnectWithError(
string $error) : void{
151 $this->session->disconnectWithError(
152 reason:
"Error downloading resource packs: " . $error,
157 public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
158 switch($packet->status){
159 case ResourcePackClientResponsePacket::STATUS_REFUSED:
161 $this->session->disconnect(
"Refused resource packs",
"You must accept resource packs to join this server.",
true);
163 case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
164 if($this->requestedMetadata){
165 throw new PacketHandlingException(
"Cannot request resource pack metadata multiple times");
167 $this->requestedMetadata =
true;
169 if($this->requestedStack){
171 throw new PacketHandlingException(
"Cannot request resource pack metadata after resource pack stack");
174 if(count($packet->packIds) > count($this->resourcePacksByPrintableId)){
175 throw new PacketHandlingException(sprintf(
"Requested metadata for more resource packs (%d) than available on the server (%d)", count($packet->packIds), count($this->resourcePacksByPrintableId)));
179 foreach($packet->packIds as $requestedPrintableId){
181 $splitPos = strpos($requestedPrintableId,
"_");
182 if($splitPos !== false){
183 $requestedPrintableId = substr($requestedPrintableId, 0, $splitPos);
185 $pack = $this->getPackByPrintableId($requestedPrintableId);
187 if(!($pack instanceof ResourcePack)){
189 $this->disconnectWithError(
"Unknown pack $requestedPrintableId requested, available packs: " . implode(
", ", array_keys($this->resourcePacksByPrintableId)));
192 $printableId = $pack->getPackId()->toString();
193 if(isset($seen[$printableId])){
194 throw new PacketHandlingException(
"Repeated metadata request for pack $requestedPrintableId");
197 $this->session->sendDataPacket(ResourcePackDataInfoPacket::create(
199 self::PACK_CHUNK_SIZE,
200 (
int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE),
201 $pack->getPackSize(),
204 ResourcePackType::RESOURCES
206 $seen[$printableId] =
true;
208 $this->session->getLogger()->debug(
"Player requested download of " . count($packet->packIds) .
" resource packs");
210 case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS:
211 if($this->requestedStack){
212 throw new PacketHandlingException(
"Cannot request resource pack stack multiple times");
214 $this->requestedStack =
true;
216 $stack = array_map(
static function(ResourcePack $pack) : ResourcePackStackEntry{
217 return new ResourcePackStackEntry($pack->getPackId()->toString(), $pack->getPackVersion(),
"");
218 }, $this->resourcePackStack);
221 foreach(self::CHEMISTRY_RESOURCE_PACKS as [$uuid, $version]){
222 $stack[] =
new ResourcePackStackEntry($uuid, $version,
"");
228 $this->session->sendDataPacket(ResourcePackStackPacket::create($stack,
false, ProtocolInfo::MINECRAFT_VERSION_NETWORK,
new Experiments([],
false),
false));
229 $this->session->getLogger()->debug(
"Applying resource pack stack");
231 case ResourcePackClientResponsePacket::STATUS_COMPLETED:
232 $this->session->getLogger()->debug(
"Resource packs sequence completed");
233 ($this->completionCallback)();
242 public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{
243 $pack = $this->getPackByPrintableId($packet->packId);
244 if(!($pack instanceof ResourcePack)){
245 $this->disconnectWithError(
"Invalid request for chunk $packet->chunkIndex of unknown pack $packet->packId, available packs: " . implode(
", ", array_keys($this->resourcePacksByPrintableId)));
249 $printableId = $pack->getPackId()->
toString();
251 if(isset($this->downloadedChunks[$printableId][$packet->chunkIndex])){
252 $this->disconnectWithError(
"Duplicate request for chunk $packet->chunkIndex of pack $packet->packId");
256 $offset = $packet->chunkIndex * self::PACK_CHUNK_SIZE;
257 if($offset < 0 || $offset >= $pack->getPackSize()){
258 $this->disconnectWithError(
"Invalid out-of-bounds request for chunk $packet->chunkIndex of $packet->packId: offset $offset, file size " . $pack->getPackSize());
262 if(!isset($this->downloadedChunks[$printableId])){
263 $this->downloadedChunks[$printableId] = [$packet->chunkIndex =>
true];
265 $this->downloadedChunks[$printableId][$packet->chunkIndex] =
true;
268 $this->requestQueue->enqueue([$pack, $packet->chunkIndex]);
269 $this->processChunkRequestQueue();
274 private function processChunkRequestQueue() : void{
275 if($this->activeRequests >= self::MAX_CONCURRENT_CHUNK_REQUESTS || $this->requestQueue->isEmpty()){
282 [$pack, $chunkIndex] = $this->requestQueue->dequeue();
284 $printableId = $pack->getPackId()->
toString();
285 $offset = $chunkIndex * self::PACK_CHUNK_SIZE;
286 $chunkData = $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE);
287 $this->activeRequests++;
289 ->sendDataPacketWithReceipt(ResourcePackChunkDataPacket::create($printableId, $chunkIndex, $offset, $chunkData))
292 $this->activeRequests--;
293 $this->processChunkRequestQueue();
297 $this->disconnectWithError(
"Plugin interrupted sending of resource packs");