PocketMine-MP 5.23.3 git-976fc63567edab7a6fb6aeae739f43cf9fe57de4
Loading...
Searching...
No Matches
ChunkCache.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\cache;
25
32use pocketmine\world\ChunkListenerNoOpTrait;
35use function is_string;
36use function spl_object_id;
37use function strlen;
38
42class ChunkCache implements ChunkListener{
44 private static array $instances = [];
45
49 public static function getInstance(World $world, Compressor $compressor) : self{
50 $worldId = spl_object_id($world);
51 $compressorId = spl_object_id($compressor);
52 if(!isset(self::$instances[$worldId])){
53 self::$instances[$worldId] = [];
54 $world->addOnUnloadCallback(static function() use ($worldId) : void{
55 foreach(self::$instances[$worldId] as $cache){
56 $cache->caches = [];
57 }
58 unset(self::$instances[$worldId]);
59 \GlobalLogger::get()->debug("Destroyed chunk packet caches for world#$worldId");
60 });
61 }
62 if(!isset(self::$instances[$worldId][$compressorId])){
63 \GlobalLogger::get()->debug("Created new chunk packet cache (world#$worldId, compressor#$compressorId)");
64 self::$instances[$worldId][$compressorId] = new self($world, $compressor);
65 }
66 return self::$instances[$worldId][$compressorId];
67 }
68
69 public static function pruneCaches() : void{
70 foreach(self::$instances as $compressorMap){
71 foreach($compressorMap as $chunkCache){
72 foreach($chunkCache->caches as $chunkHash => $promise){
73 if(is_string($promise)){
74 //Do not clear promises that are not yet fulfilled; they will have requesters waiting on them
75 unset($chunkCache->caches[$chunkHash]);
76 }
77 }
78 }
79 }
80 }
81
86 private array $caches = [];
87
88 private int $hits = 0;
89 private int $misses = 0;
90
91 private function __construct(
92 private World $world,
93 private Compressor $compressor
94 ){}
95
96 private function prepareChunkAsync(int $chunkX, int $chunkZ, int $chunkHash) : CompressBatchPromise{
97 $this->world->registerChunkListener($this, $chunkX, $chunkZ);
98 $chunk = $this->world->getChunk($chunkX, $chunkZ);
99 if($chunk === null){
100 throw new \InvalidArgumentException("Cannot request an unloaded chunk");
101 }
102 ++$this->misses;
103
104 $this->world->timings->syncChunkSendPrepare->startTiming();
105 try{
106 $promise = new CompressBatchPromise();
107
108 $this->world->getServer()->getAsyncPool()->submitTask(
109 new ChunkRequestTask(
110 $chunkX,
111 $chunkZ,
112 DimensionIds::OVERWORLD, //TODO: not hardcode this
113 $chunk,
114 $promise,
115 $this->compressor
116 )
117 );
118 $this->caches[$chunkHash] = $promise;
119 $promise->onResolve(function(CompressBatchPromise $promise) use ($chunkHash) : void{
120 //the promise may have been discarded or replaced if the chunk was unloaded or modified in the meantime
121 if(($this->caches[$chunkHash] ?? null) === $promise){
122 $this->caches[$chunkHash] = $promise->getResult();
123 }
124 });
125
126 return $promise;
127 }finally{
128 $this->world->timings->syncChunkSendPrepare->stopTiming();
129 }
130 }
131
137 public function request(int $chunkX, int $chunkZ) : CompressBatchPromise|string{
138 $chunkHash = World::chunkHash($chunkX, $chunkZ);
139 if(isset($this->caches[$chunkHash])){
140 ++$this->hits;
141 return $this->caches[$chunkHash];
142 }
143
144 return $this->prepareChunkAsync($chunkX, $chunkZ, $chunkHash);
145 }
146
147 private function destroy(int $chunkX, int $chunkZ) : bool{
148 $chunkHash = World::chunkHash($chunkX, $chunkZ);
149 $existing = $this->caches[$chunkHash] ?? null;
150 unset($this->caches[$chunkHash]);
151
152 return $existing !== null;
153 }
154
158 private function destroyOrRestart(int $chunkX, int $chunkZ) : void{
159 $chunkPosHash = World::chunkHash($chunkX, $chunkZ);
160 $cache = $this->caches[$chunkPosHash] ?? null;
161 if($cache !== null){
162 if(!is_string($cache)){
163 //some requesters are waiting for this chunk, so their request needs to be fulfilled
164 $cache->cancel();
165 unset($this->caches[$chunkPosHash]);
166
167 $this->prepareChunkAsync($chunkX, $chunkZ, $chunkPosHash)->onResolve(...$cache->getResolveCallbacks());
168 }else{
169 //dump the cache, it'll be regenerated the next time it's requested
170 $this->destroy($chunkX, $chunkZ);
171 }
172 }
173 }
174
175 use ChunkListenerNoOpTrait {
176 //force overriding of these
177 onChunkChanged as private;
178 onBlockChanged as private;
179 onChunkUnloaded as private;
180 }
181
185 public function onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk) : void{
186 $this->destroyOrRestart($chunkX, $chunkZ);
187 }
188
192 public function onBlockChanged(Vector3 $block) : void{
193 //FIXME: requesters will still receive this chunk after it's been dropped, but we can't mark this for a simple
194 //sync here because it can spam the worker pool
195 $this->destroy($block->getFloorX() >> Chunk::COORD_BIT_SIZE, $block->getFloorZ() >> Chunk::COORD_BIT_SIZE);
196 }
197
201 public function onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{
202 $this->destroy($chunkX, $chunkZ);
203 $this->world->unregisterChunkListener($this, $chunkX, $chunkZ);
204 }
205
210 public function calculateCacheSize() : int{
211 $result = 0;
212 foreach($this->caches as $cache){
213 if(is_string($cache)){
214 $result += strlen($cache);
215 }
216 }
217 return $result;
218 }
219
223 public function getHitPercentage() : float{
224 $total = $this->hits + $this->misses;
225 return $total > 0 ? $this->hits / $total : 0.0;
226 }
227}
onBlockChanged as onChunkUnloaded as onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk)
request(int $chunkX, int $chunkZ)
onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk)
static getInstance(World $world, Compressor $compressor)
addOnUnloadCallback(\Closure $callback)
Definition World.php:668