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");