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