PocketMine-MP 5.39.3 git-21ae710729750cd637333d673bbbbbc598fc659e
Loading...
Searching...
No Matches
ResourcePacksPacketHandler.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\network\mcpe\handler;
25
41use Ramsey\Uuid\Uuid;
42use function array_keys;
43use function array_map;
44use function ceil;
45use function count;
46use function implode;
47use function sprintf;
48use function strpos;
49use function strtolower;
50use function substr;
51
57 private const PACK_CHUNK_SIZE = 256 * 1024; //256KB
58
63 private const MAX_CONCURRENT_CHUNK_REQUESTS = 1;
64
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"],
77 ];
78
83 private array $resourcePacksByPrintableId = [];
84
85 private bool $requestedMetadata = false;
86 private bool $requestedStack = false;
87
89 private array $downloadedChunks = [];
90
92 private \SplQueue $requestQueue;
93
94 private int $activeRequests = 0;
95
104 public function __construct(
105 private NetworkSession $session,
106 private array $resourcePackStack,
107 private array $encryptionKeys,
108 private bool $mustAccept,
109 private \Closure $completionCallback
110 ){
111 $this->requestQueue = new \SplQueue();
112 foreach($resourcePackStack as $pack){
113 $this->resourcePacksByPrintableId[$pack->getPackId()->toString()] = $pack;
114 }
115 }
116
117 private function getPackByPrintableId(string $printableId) : ?ResourcePack{
118 return $this->resourcePacksByPrintableId[strtolower($printableId)] ?? null;
119 }
120
121 public function setUp() : void{
122 $resourcePackEntries = array_map(function(ResourcePack $pack) : ResourcePackInfoEntry{
123 //TODO: more stuff
124
125 $uuid = $pack->getPackId();
126 $printableId = $uuid->toString();
127 return new ResourcePackInfoEntry(
128 $uuid,
129 $pack->getPackVersion(),
130 $pack->getPackSize(),
131 $this->encryptionKeys[$printableId] ?? "",
132 "",
133 $printableId,
134 false
135 );
136 }, $this->resourcePackStack);
137 //TODO: support forcing server packs
138 $this->session->sendDataPacket(ResourcePacksInfoPacket::create(
139 resourcePackEntries: $resourcePackEntries,
140 mustAccept: $this->mustAccept,
141 hasAddons: false,
142 hasScripts: false,
143 worldTemplateId: Uuid::fromString(Uuid::NIL),
144 worldTemplateVersion: "",
145 forceDisableVibrantVisuals: true,
146 ));
147 $this->session->getLogger()->debug("Waiting for client to accept resource packs");
148 }
149
150 private function disconnectWithError(string $error) : void{
151 $this->session->disconnectWithError(
152 reason: "Error downloading resource packs: " . $error,
153 disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_resourcePack()
154 );
155 }
156
157 public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
158 switch($packet->status){
159 case ResourcePackClientResponsePacket::STATUS_REFUSED:
160 //TODO: add lang strings for this
161 $this->session->disconnect("Refused resource packs", "You must accept resource packs to join this server.", true);
162 break;
163 case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
164 if($this->requestedMetadata){
165 throw new PacketHandlingException("Cannot request resource pack metadata multiple times");
166 }
167 $this->requestedMetadata = true;
168
169 if($this->requestedStack){
170 //client already told us that they have all the packs, they shouldn't be asking for more
171 throw new PacketHandlingException("Cannot request resource pack metadata after resource pack stack");
172 }
173
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)));
176 }
177
178 $seen = [];
179 foreach($packet->packIds as $requestedPrintableId){
180 //dirty hack for mojang's dirty hack for versions
181 $splitPos = strpos($requestedPrintableId, "_");
182 if($splitPos !== false){
183 $requestedPrintableId = substr($requestedPrintableId, 0, $splitPos);
184 }
185 $pack = $this->getPackByPrintableId($requestedPrintableId);
186
187 if(!($pack instanceof ResourcePack)){
188 //Client requested a resource pack but we don't have it available on the server
189 $this->disconnectWithError("Unknown pack $requestedPrintableId requested, available packs: " . implode(", ", array_keys($this->resourcePacksByPrintableId)));
190 return false;
191 }
192 $printableId = $pack->getPackId()->toString();
193 if(isset($seen[$printableId])){
194 throw new PacketHandlingException("Repeated metadata request for pack $requestedPrintableId");
195 }
196
197 $this->session->sendDataPacket(ResourcePackDataInfoPacket::create(
198 $printableId,
199 self::PACK_CHUNK_SIZE,
200 (int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE),
201 $pack->getPackSize(),
202 $pack->getSha256(),
203 false,
204 ResourcePackType::RESOURCES //TODO: this might be an addon (not behaviour pack), needed to properly support client-side custom items
205 ));
206 $seen[$printableId] = true;
207 }
208 $this->session->getLogger()->debug("Player requested download of " . count($packet->packIds) . " resource packs");
209 break;
210 case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS:
211 if($this->requestedStack){
212 throw new PacketHandlingException("Cannot request resource pack stack multiple times");
213 }
214 $this->requestedStack = true;
215
216 $stack = array_map(static function(ResourcePack $pack) : ResourcePackStackEntry{
217 return new ResourcePackStackEntry($pack->getPackId()->toString(), $pack->getPackVersion(), ""); //TODO: subpacks
218 }, $this->resourcePackStack);
219
220 //we support chemistry blocks by default, the client should already have these installed
221 foreach(self::CHEMISTRY_RESOURCE_PACKS as [$uuid, $version]){
222 $stack[] = new ResourcePackStackEntry($uuid, $version, "");
223 }
224
225 //we don't force here, because it doesn't have user-facing effects
226 //but it does have an annoying side-effect when true: it makes
227 //the client remove its own non-server-supplied resource packs.
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");
230 break;
231 case ResourcePackClientResponsePacket::STATUS_COMPLETED:
232 $this->session->getLogger()->debug("Resource packs sequence completed");
233 ($this->completionCallback)();
234 break;
235 default:
236 return false;
237 }
238
239 return true;
240 }
241
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)));
246 return false;
247 }
248
249 $printableId = $pack->getPackId()->toString(); //use this because case may be different in the packet vs locally
250
251 if(isset($this->downloadedChunks[$printableId][$packet->chunkIndex])){
252 $this->disconnectWithError("Duplicate request for chunk $packet->chunkIndex of pack $packet->packId");
253 return false;
254 }
255
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());
259 return false;
260 }
261
262 if(!isset($this->downloadedChunks[$printableId])){
263 $this->downloadedChunks[$printableId] = [$packet->chunkIndex => true];
264 }else{
265 $this->downloadedChunks[$printableId][$packet->chunkIndex] = true;
266 }
267
268 $this->requestQueue->enqueue([$pack, $packet->chunkIndex]);
269 $this->processChunkRequestQueue();
270
271 return true;
272 }
273
274 private function processChunkRequestQueue() : void{
275 if($this->activeRequests >= self::MAX_CONCURRENT_CHUNK_REQUESTS || $this->requestQueue->isEmpty()){
276 return;
277 }
282 [$pack, $chunkIndex] = $this->requestQueue->dequeue();
283
284 $printableId = $pack->getPackId()->toString();
285 $offset = $chunkIndex * self::PACK_CHUNK_SIZE;
286 $chunkData = $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE);
287 $this->activeRequests++;
288 $this->session
289 ->sendDataPacketWithReceipt(ResourcePackChunkDataPacket::create($printableId, $chunkIndex, $offset, $chunkData))
290 ->onCompletion(
291 function() : void{
292 $this->activeRequests--;
293 $this->processChunkRequestQueue();
294 },
295 function() : void{
296 //this may have been rejected because of a disconnection - this will do nothing in that case
297 $this->disconnectWithError("Plugin interrupted sending of resource packs");
298 }
299 );
300 }
301}
__construct(private NetworkSession $session, private array $resourcePackStack, private array $encryptionKeys, private bool $mustAccept, private \Closure $completionCallback)