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