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