world = $world; } /** * Requests asynchronous preparation of the chunk at the given coordinates. * * @param int $chunkX * @param int $chunkZ * * @return CompressBatchPromise a promise of resolution which will contain a compressed chunk packet. */ public function request(int $chunkX, int $chunkZ) : CompressBatchPromise{ $this->world->registerChunkListener($this, $chunkX, $chunkZ); $chunkHash = World::chunkHash($chunkX, $chunkZ); if(isset($this->caches[$chunkHash])){ ++$this->hits; return $this->caches[$chunkHash]; } ++$this->misses; $this->world->timings->syncChunkSendPrepareTimer->startTiming(); try{ $this->caches[$chunkHash] = new CompressBatchPromise(); $this->world->getServer()->getAsyncPool()->submitTask( new ChunkRequestTask( $chunkX, $chunkZ, $this->world->getChunk($chunkX, $chunkZ), $this->caches[$chunkHash], function() use ($chunkX, $chunkZ){ $this->world->getLogger()->error("Failed preparing chunk $chunkX $chunkZ, retrying"); $this->restartPendingRequest($chunkX, $chunkZ); } ) ); return $this->caches[$chunkHash]; }finally{ $this->world->timings->syncChunkSendPrepareTimer->stopTiming(); } } private function destroy(int $chunkX, int $chunkZ) : bool{ $chunkHash = World::chunkHash($chunkX, $chunkZ); $existing = $this->caches[$chunkHash] ?? null; unset($this->caches[$chunkHash]); return $existing !== null; } /** * Restarts an async request for an unresolved chunk. * * @param int $chunkX * @param int $chunkZ * * @throws \InvalidArgumentException */ private function restartPendingRequest(int $chunkX, int $chunkZ) : void{ $chunkHash = World::chunkHash($chunkX, $chunkZ); $existing = $this->caches[$chunkHash] ?? null; if($existing === null or $existing->hasResult()){ throw new \InvalidArgumentException("Restart can only be applied to unresolved promises"); } $existing->cancel(); unset($this->caches[$chunkHash]); $this->request($chunkX, $chunkZ)->onResolve(...$existing->getResolveCallbacks()); } /** * @param int $chunkX * @param int $chunkZ * * @throws \InvalidArgumentException */ private function destroyOrRestart(int $chunkX, int $chunkZ) : void{ $cache = $this->caches[World::chunkHash($chunkX, $chunkZ)] ?? null; if($cache !== null){ if(!$cache->hasResult()){ //some requesters are waiting for this chunk, so their request needs to be fulfilled $this->restartPendingRequest($chunkX, $chunkZ); }else{ //dump the cache, it'll be regenerated the next time it's requested $this->destroy($chunkX, $chunkZ); } } } use ChunkListenerNoOpTrait { //force overriding of these onChunkChanged as private; onBlockChanged as private; onChunkUnloaded as private; } /** * @see ChunkListener::onChunkChanged() * @param Chunk $chunk */ public function onChunkChanged(Chunk $chunk) : void{ //FIXME: this gets fired for stuff that doesn't change terrain related things (like lighting updates) $this->destroyOrRestart($chunk->getX(), $chunk->getZ()); } /** * @see ChunkListener::onBlockChanged() * @param Vector3 $block */ public function onBlockChanged(Vector3 $block) : void{ //FIXME: requesters will still receive this chunk after it's been dropped, but we can't mark this for a simple //sync here because it can spam the worker pool $this->destroy($block->getFloorX() >> 4, $block->getFloorZ() >> 4); } /** * @see ChunkListener::onChunkUnloaded() * @param Chunk $chunk */ public function onChunkUnloaded(Chunk $chunk) : void{ $this->destroy($chunk->getX(), $chunk->getZ()); $this->world->unregisterChunkListener($this, $chunk->getX(), $chunk->getZ()); } /** * Returns the number of bytes occupied by the cache data in this cache. This does not include the size of any * promises referenced by the cache. * * @return int */ public function calculateCacheSize() : int{ $result = 0; foreach($this->caches as $cache){ if($cache->hasResult()){ $result += strlen($cache->getResult()); } } return $result; } /** * Returns the percentage of requests to the cache which resulted in a cache hit. * * @return float */ public function getHitPercentage() : float{ $total = $this->hits + $this->misses; return $total > 0 ? $this->hits / $total : 0.0; } }