PocketMine-MP 5.32.2 git-237b304ef9858756b018e44e8f298093f66f823b
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 array $resourcePacksById = [];
70
71 private bool $requestedMetadata = false;
72 private bool $requestedStack = false;
73
75 private array $downloadedChunks = [];
76
78 private \SplQueue $requestQueue;
79
80 private int $activeRequests = 0;
81
90 public function __construct(
91 private NetworkSession $session,
92 private array $resourcePackStack,
93 private array $encryptionKeys,
94 private bool $mustAccept,
95 private \Closure $completionCallback
96 ){
97 $this->requestQueue = new \SplQueue();
98 foreach($resourcePackStack as $pack){
99 $this->resourcePacksById[$pack->getPackId()] = $pack;
100 }
101 }
102
103 private function getPackById(string $id) : ?ResourcePack{
104 return $this->resourcePacksById[strtolower($id)] ?? null;
105 }
106
107 public function setUp() : void{
108 $resourcePackEntries = array_map(function(ResourcePack $pack) : ResourcePackInfoEntry{
109 //TODO: more stuff
110
111 return new ResourcePackInfoEntry(
112 Uuid::fromString($pack->getPackId()),
113 $pack->getPackVersion(),
114 $pack->getPackSize(),
115 $this->encryptionKeys[$pack->getPackId()] ?? "",
116 "",
117 $pack->getPackId(),
118 false
119 );
120 }, $this->resourcePackStack);
121 //TODO: support forcing server packs
122 $this->session->sendDataPacket(ResourcePacksInfoPacket::create(
123 resourcePackEntries: $resourcePackEntries,
124 mustAccept: $this->mustAccept,
125 hasAddons: false,
126 hasScripts: false,
127 worldTemplateId: Uuid::fromString(Uuid::NIL),
128 worldTemplateVersion: "",
129 forceDisableVibrantVisuals: true,
130 ));
131 $this->session->getLogger()->debug("Waiting for client to accept resource packs");
132 }
133
134 private function disconnectWithError(string $error) : void{
135 $this->session->disconnectWithError(
136 reason: "Error downloading resource packs: " . $error,
137 disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_resourcePack()
138 );
139 }
140
141 public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
142 switch($packet->status){
143 case ResourcePackClientResponsePacket::STATUS_REFUSED:
144 //TODO: add lang strings for this
145 $this->session->disconnect("Refused resource packs", "You must accept resource packs to join this server.", true);
146 break;
147 case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
148 if($this->requestedMetadata){
149 throw new PacketHandlingException("Cannot request resource pack metadata multiple times");
150 }
151 $this->requestedMetadata = true;
152
153 if($this->requestedStack){
154 //client already told us that they have all the packs, they shouldn't be asking for more
155 throw new PacketHandlingException("Cannot request resource pack metadata after resource pack stack");
156 }
157
158 if(count($packet->packIds) > count($this->resourcePacksById)){
159 throw new PacketHandlingException(sprintf("Requested metadata for more resource packs (%d) than available on the server (%d)", count($packet->packIds), count($this->resourcePacksById)));
160 }
161
162 $seen = [];
163 foreach($packet->packIds as $uuid){
164 //dirty hack for mojang's dirty hack for versions
165 $splitPos = strpos($uuid, "_");
166 if($splitPos !== false){
167 $uuid = substr($uuid, 0, $splitPos);
168 }
169 $pack = $this->getPackById($uuid);
170
171 if(!($pack instanceof ResourcePack)){
172 //Client requested a resource pack but we don't have it available on the server
173 $this->disconnectWithError("Unknown pack $uuid requested, available packs: " . implode(", ", array_keys($this->resourcePacksById)));
174 return false;
175 }
176 if(isset($seen[$pack->getPackId()])){
177 throw new PacketHandlingException("Repeated metadata request for pack $uuid");
178 }
179
180 $this->session->sendDataPacket(ResourcePackDataInfoPacket::create(
181 $pack->getPackId(),
182 self::PACK_CHUNK_SIZE,
183 (int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE),
184 $pack->getPackSize(),
185 $pack->getSha256(),
186 false,
187 ResourcePackType::RESOURCES //TODO: this might be an addon (not behaviour pack), needed to properly support client-side custom items
188 ));
189 $seen[$pack->getPackId()] = true;
190 }
191 $this->session->getLogger()->debug("Player requested download of " . count($packet->packIds) . " resource packs");
192 break;
193 case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS:
194 if($this->requestedStack){
195 throw new PacketHandlingException("Cannot request resource pack stack multiple times");
196 }
197 $this->requestedStack = true;
198
199 $stack = array_map(static function(ResourcePack $pack) : ResourcePackStackEntry{
200 return new ResourcePackStackEntry($pack->getPackId(), $pack->getPackVersion(), ""); //TODO: subpacks
201 }, $this->resourcePackStack);
202
203 //we support chemistry blocks by default, the client should already have this installed
204 $stack[] = new ResourcePackStackEntry("0fba4063-dba1-4281-9b89-ff9390653530", "1.0.0", "");
205
206 //we don't force here, because it doesn't have user-facing effects
207 //but it does have an annoying side-effect when true: it makes
208 //the client remove its own non-server-supplied resource packs.
209 $this->session->sendDataPacket(ResourcePackStackPacket::create($stack, [], false, ProtocolInfo::MINECRAFT_VERSION_NETWORK, new Experiments([], false), false));
210 $this->session->getLogger()->debug("Applying resource pack stack");
211 break;
212 case ResourcePackClientResponsePacket::STATUS_COMPLETED:
213 $this->session->getLogger()->debug("Resource packs sequence completed");
214 ($this->completionCallback)();
215 break;
216 default:
217 return false;
218 }
219
220 return true;
221 }
222
223 public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{
224 $pack = $this->getPackById($packet->packId);
225 if(!($pack instanceof ResourcePack)){
226 $this->disconnectWithError("Invalid request for chunk $packet->chunkIndex of unknown pack $packet->packId, available packs: " . implode(", ", array_keys($this->resourcePacksById)));
227 return false;
228 }
229
230 $packId = $pack->getPackId(); //use this because case may be different
231
232 if(isset($this->downloadedChunks[$packId][$packet->chunkIndex])){
233 $this->disconnectWithError("Duplicate request for chunk $packet->chunkIndex of pack $packet->packId");
234 return false;
235 }
236
237 $offset = $packet->chunkIndex * self::PACK_CHUNK_SIZE;
238 if($offset < 0 || $offset >= $pack->getPackSize()){
239 $this->disconnectWithError("Invalid out-of-bounds request for chunk $packet->chunkIndex of $packet->packId: offset $offset, file size " . $pack->getPackSize());
240 return false;
241 }
242
243 if(!isset($this->downloadedChunks[$packId])){
244 $this->downloadedChunks[$packId] = [$packet->chunkIndex => true];
245 }else{
246 $this->downloadedChunks[$packId][$packet->chunkIndex] = true;
247 }
248
249 $this->requestQueue->enqueue([$pack, $packet->chunkIndex]);
250 $this->processChunkRequestQueue();
251
252 return true;
253 }
254
255 private function processChunkRequestQueue() : void{
256 if($this->activeRequests >= self::MAX_CONCURRENT_CHUNK_REQUESTS || $this->requestQueue->isEmpty()){
257 return;
258 }
263 [$pack, $chunkIndex] = $this->requestQueue->dequeue();
264
265 $packId = $pack->getPackId();
266 $offset = $chunkIndex * self::PACK_CHUNK_SIZE;
267 $chunkData = $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE);
268 $this->activeRequests++;
269 $this->session
270 ->sendDataPacketWithReceipt(ResourcePackChunkDataPacket::create($packId, $chunkIndex, $offset, $chunkData))
271 ->onCompletion(
272 function() : void{
273 $this->activeRequests--;
274 $this->processChunkRequestQueue();
275 },
276 function() : void{
277 //this may have been rejected because of a disconnection - this will do nothing in that case
278 $this->disconnectWithError("Plugin interrupted sending of resource packs");
279 }
280 );
281 }
282}
__construct(private NetworkSession $session, private array $resourcePackStack, private array $encryptionKeys, private bool $mustAccept, private \Closure $completionCallback)