PocketMine-MP 5.35.1 git-e32e836dad793a3a3c8ddd8927c00e112b1e576a
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 $resourcePacksById = [];
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->resourcePacksById[$pack->getPackId()] = $pack;
114 }
115 }
116
117 private function getPackById(string $id) : ?ResourcePack{
118 return $this->resourcePacksById[strtolower($id)] ?? null;
119 }
120
121 public function setUp() : void{
122 $resourcePackEntries = array_map(function(ResourcePack $pack) : ResourcePackInfoEntry{
123 //TODO: more stuff
124
125 return new ResourcePackInfoEntry(
126 Uuid::fromString($pack->getPackId()),
127 $pack->getPackVersion(),
128 $pack->getPackSize(),
129 $this->encryptionKeys[$pack->getPackId()] ?? "",
130 "",
131 $pack->getPackId(),
132 false
133 );
134 }, $this->resourcePackStack);
135 //TODO: support forcing server packs
136 $this->session->sendDataPacket(ResourcePacksInfoPacket::create(
137 resourcePackEntries: $resourcePackEntries,
138 mustAccept: $this->mustAccept,
139 hasAddons: false,
140 hasScripts: false,
141 worldTemplateId: Uuid::fromString(Uuid::NIL),
142 worldTemplateVersion: "",
143 forceDisableVibrantVisuals: true,
144 ));
145 $this->session->getLogger()->debug("Waiting for client to accept resource packs");
146 }
147
148 private function disconnectWithError(string $error) : void{
149 $this->session->disconnectWithError(
150 reason: "Error downloading resource packs: " . $error,
151 disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_resourcePack()
152 );
153 }
154
155 public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
156 switch($packet->status){
157 case ResourcePackClientResponsePacket::STATUS_REFUSED:
158 //TODO: add lang strings for this
159 $this->session->disconnect("Refused resource packs", "You must accept resource packs to join this server.", true);
160 break;
161 case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
162 if($this->requestedMetadata){
163 throw new PacketHandlingException("Cannot request resource pack metadata multiple times");
164 }
165 $this->requestedMetadata = true;
166
167 if($this->requestedStack){
168 //client already told us that they have all the packs, they shouldn't be asking for more
169 throw new PacketHandlingException("Cannot request resource pack metadata after resource pack stack");
170 }
171
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)));
174 }
175
176 $seen = [];
177 foreach($packet->packIds as $uuid){
178 //dirty hack for mojang's dirty hack for versions
179 $splitPos = strpos($uuid, "_");
180 if($splitPos !== false){
181 $uuid = substr($uuid, 0, $splitPos);
182 }
183 $pack = $this->getPackById($uuid);
184
185 if(!($pack instanceof ResourcePack)){
186 //Client requested a resource pack but we don't have it available on the server
187 $this->disconnectWithError("Unknown pack $uuid requested, available packs: " . implode(", ", array_keys($this->resourcePacksById)));
188 return false;
189 }
190 if(isset($seen[$pack->getPackId()])){
191 throw new PacketHandlingException("Repeated metadata request for pack $uuid");
192 }
193
194 $this->session->sendDataPacket(ResourcePackDataInfoPacket::create(
195 $pack->getPackId(),
196 self::PACK_CHUNK_SIZE,
197 (int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE),
198 $pack->getPackSize(),
199 $pack->getSha256(),
200 false,
201 ResourcePackType::RESOURCES //TODO: this might be an addon (not behaviour pack), needed to properly support client-side custom items
202 ));
203 $seen[$pack->getPackId()] = true;
204 }
205 $this->session->getLogger()->debug("Player requested download of " . count($packet->packIds) . " resource packs");
206 break;
207 case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS:
208 if($this->requestedStack){
209 throw new PacketHandlingException("Cannot request resource pack stack multiple times");
210 }
211 $this->requestedStack = true;
212
213 $stack = array_map(static function(ResourcePack $pack) : ResourcePackStackEntry{
214 return new ResourcePackStackEntry($pack->getPackId(), $pack->getPackVersion(), ""); //TODO: subpacks
215 }, $this->resourcePackStack);
216
217 //we support chemistry blocks by default, the client should already have these installed
218 foreach(self::CHEMISTRY_RESOURCE_PACKS as [$uuid, $version]){
219 $stack[] = new ResourcePackStackEntry($uuid, $version, "");
220 }
221
222 //we don't force here, because it doesn't have user-facing effects
223 //but it does have an annoying side-effect when true: it makes
224 //the client remove its own non-server-supplied resource packs.
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");
227 break;
228 case ResourcePackClientResponsePacket::STATUS_COMPLETED:
229 $this->session->getLogger()->debug("Resource packs sequence completed");
230 ($this->completionCallback)();
231 break;
232 default:
233 return false;
234 }
235
236 return true;
237 }
238
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)));
243 return false;
244 }
245
246 $packId = $pack->getPackId(); //use this because case may be different
247
248 if(isset($this->downloadedChunks[$packId][$packet->chunkIndex])){
249 $this->disconnectWithError("Duplicate request for chunk $packet->chunkIndex of pack $packet->packId");
250 return false;
251 }
252
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());
256 return false;
257 }
258
259 if(!isset($this->downloadedChunks[$packId])){
260 $this->downloadedChunks[$packId] = [$packet->chunkIndex => true];
261 }else{
262 $this->downloadedChunks[$packId][$packet->chunkIndex] = true;
263 }
264
265 $this->requestQueue->enqueue([$pack, $packet->chunkIndex]);
266 $this->processChunkRequestQueue();
267
268 return true;
269 }
270
271 private function processChunkRequestQueue() : void{
272 if($this->activeRequests >= self::MAX_CONCURRENT_CHUNK_REQUESTS || $this->requestQueue->isEmpty()){
273 return;
274 }
279 [$pack, $chunkIndex] = $this->requestQueue->dequeue();
280
281 $packId = $pack->getPackId();
282 $offset = $chunkIndex * self::PACK_CHUNK_SIZE;
283 $chunkData = $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE);
284 $this->activeRequests++;
285 $this->session
286 ->sendDataPacketWithReceipt(ResourcePackChunkDataPacket::create($packId, $chunkIndex, $offset, $chunkData))
287 ->onCompletion(
288 function() : void{
289 $this->activeRequests--;
290 $this->processChunkRequestQueue();
291 },
292 function() : void{
293 //this may have been rejected because of a disconnection - this will do nothing in that case
294 $this->disconnectWithError("Plugin interrupted sending of resource packs");
295 }
296 );
297 }
298}
__construct(private NetworkSession $session, private array $resourcePackStack, private array $encryptionKeys, private bool $mustAccept, private \Closure $completionCallback)