Relocate cache-related classes to mcpe\cache namespace

This commit is contained in:
Dylan K. Taylor
2020-11-16 19:39:30 +00:00
parent 869c9dabf1
commit 1d27225553
5 changed files with 8 additions and 6 deletions

225
src/network/mcpe/cache/ChunkCache.php vendored Normal file
View File

@ -0,0 +1,225 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\cache;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\ChunkRequestTask;
use pocketmine\network\mcpe\compression\CompressBatchPromise;
use pocketmine\network\mcpe\compression\Compressor;
use pocketmine\world\ChunkListener;
use pocketmine\world\ChunkListenerNoOpTrait;
use pocketmine\world\format\Chunk;
use pocketmine\world\World;
use function spl_object_id;
use function strlen;
/**
* This class is used by the current MCPE protocol system to store cached chunk packets for fast resending.
*
* TODO: make MemoryManager aware of this so the cache can be destroyed when memory is low
* TODO: this needs a hook for world unloading
*/
class ChunkCache implements ChunkListener{
/** @var self[][] */
private static $instances = [];
/**
* Fetches the ChunkCache instance for the given world. This lazily creates cache systems as needed.
*
* @return ChunkCache
*/
public static function getInstance(World $world, Compressor $compressor) : self{
$worldId = spl_object_id($world);
$compressorId = spl_object_id($compressor);
if(!isset(self::$instances[$worldId])){
self::$instances[$worldId] = [];
$world->addOnUnloadCallback(static function() use ($worldId) : void{
foreach(self::$instances[$worldId] as $cache){
$cache->caches = [];
}
unset(self::$instances[$worldId]);
\GlobalLogger::get()->debug("Destroyed chunk packet caches for world#$worldId");
});
}
if(!isset(self::$instances[$worldId][$compressorId])){
\GlobalLogger::get()->debug("Created new chunk packet cache (world#$worldId, compressor#$compressorId)");
self::$instances[$worldId][$compressorId] = new self($world, $compressor);
}
return self::$instances[$worldId][$compressorId];
}
/** @var World */
private $world;
/** @var Compressor */
private $compressor;
/** @var CompressBatchPromise[] */
private $caches = [];
/** @var int */
private $hits = 0;
/** @var int */
private $misses = 0;
private function __construct(World $world, Compressor $compressor){
$this->world = $world;
$this->compressor = $compressor;
}
/**
* Requests asynchronous preparation of the chunk at the given coordinates.
*
* @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],
$this->compressor,
function() use ($chunkX, $chunkZ) : void{
$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.
*
* @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());
}
/**
* @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()
*/
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()
*/
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()
*/
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.
*/
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.
*/
public function getHitPercentage() : float{
$total = $this->hits + $this->misses;
return $total > 0 ? $this->hits / $total : 0.0;
}
}

View File

@ -0,0 +1,132 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\cache;
use pocketmine\crafting\CraftingManager;
use pocketmine\item\Item;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\protocol\CraftingDataPacket;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipe as ProtocolFurnaceRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient;
use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe as ProtocolShapedRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe;
use pocketmine\timings\Timings;
use pocketmine\utils\Binary;
use pocketmine\utils\SingletonTrait;
use pocketmine\uuid\UUID;
use function array_map;
use function spl_object_id;
use function str_repeat;
final class CraftingDataCache{
use SingletonTrait;
/**
* @var CraftingDataPacket[]
* @phpstan-var array<int, CraftingDataPacket>
*/
private $caches = [];
public function getCache(CraftingManager $manager) : CraftingDataPacket{
$id = spl_object_id($manager);
if(!isset($this->caches[$id])){
$manager->getDestructorCallbacks()->add(function() use ($id) : void{
unset($this->caches[$id]);
});
$manager->getRecipeRegisteredCallbacks()->add(function() use ($id) : void{
unset($this->caches[$id]);
});
$this->caches[$id] = $this->buildCraftingDataCache($manager);
}
return $this->caches[$id];
}
/**
* Rebuilds the cached CraftingDataPacket.
*/
private function buildCraftingDataCache(CraftingManager $manager) : CraftingDataPacket{
Timings::$craftingDataCacheRebuildTimer->startTiming();
$pk = new CraftingDataPacket();
$pk->cleanRecipes = true;
$counter = 0;
$nullUUID = UUID::fromData(str_repeat("\x00", 16));
$converter = TypeConverter::getInstance();
foreach($manager->getShapelessRecipes() as $list){
foreach($list as $recipe){
$pk->entries[] = new ProtocolShapelessRecipe(
CraftingDataPacket::ENTRY_SHAPELESS,
Binary::writeInt($counter++),
array_map(function(Item $item) use ($converter) : RecipeIngredient{
return $converter->coreItemStackToRecipeIngredient($item);
}, $recipe->getIngredientList()),
array_map(function(Item $item) use ($converter) : ItemStack{
return $converter->coreItemStackToNet($item);
}, $recipe->getResults()),
$nullUUID,
"crafting_table",
50,
$counter
);
}
}
foreach($manager->getShapedRecipes() as $list){
foreach($list as $recipe){
$inputs = [];
for($row = 0, $height = $recipe->getHeight(); $row < $height; ++$row){
for($column = 0, $width = $recipe->getWidth(); $column < $width; ++$column){
$inputs[$row][$column] = $converter->coreItemStackToRecipeIngredient($recipe->getIngredient($column, $row));
}
}
$pk->entries[] = $r = new ProtocolShapedRecipe(
CraftingDataPacket::ENTRY_SHAPED,
Binary::writeInt($counter++),
$inputs,
array_map(function(Item $item) use ($converter) : ItemStack{
return $converter->coreItemStackToNet($item);
}, $recipe->getResults()),
$nullUUID,
"crafting_table",
50,
$counter
);
}
}
foreach($manager->getFurnaceRecipeManager()->getAll() as $recipe){
$input = $converter->coreItemStackToNet($recipe->getInput());
$pk->entries[] = new ProtocolFurnaceRecipe(
CraftingDataPacket::ENTRY_FURNACE_DATA,
$input->getId(),
$input->getMeta(),
$converter->coreItemStackToNet($recipe->getResult()),
"furnace"
);
}
return $pk;
}
}

View File

@ -0,0 +1,69 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\cache;
use pocketmine\network\mcpe\protocol\AvailableActorIdentifiersPacket;
use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use pocketmine\network\mcpe\protocol\types\CacheableNbt;
use pocketmine\utils\SingletonTrait;
use function file_get_contents;
class StaticPacketCache{
use SingletonTrait;
/** @var BiomeDefinitionListPacket */
private $biomeDefs;
/** @var AvailableActorIdentifiersPacket */
private $availableActorIdentifiers;
/**
* @phpstan-return CacheableNbt<\pocketmine\nbt\tag\CompoundTag>
*/
private static function loadCompoundFromFile(string $filePath) : CacheableNbt{
$rawNbt = @file_get_contents($filePath);
if($rawNbt === false) throw new \RuntimeException("Failed to read file");
return new CacheableNbt((new NetworkNbtSerializer())->read($rawNbt)->mustGetCompoundTag());
}
private static function make() : self{
return new self(
BiomeDefinitionListPacket::create(self::loadCompoundFromFile(\pocketmine\RESOURCE_PATH . '/vanilla/biome_definitions.nbt')),
AvailableActorIdentifiersPacket::create(self::loadCompoundFromFile(\pocketmine\RESOURCE_PATH . '/vanilla/entity_identifiers.nbt'))
);
}
public function __construct(BiomeDefinitionListPacket $biomeDefs, AvailableActorIdentifiersPacket $availableActorIdentifiers){
$this->biomeDefs = $biomeDefs;
$this->availableActorIdentifiers = $availableActorIdentifiers;
}
public function getBiomeDefs() : BiomeDefinitionListPacket{
return $this->biomeDefs;
}
public function getAvailableActorIdentifiers() : AvailableActorIdentifiersPacket{
return $this->availableActorIdentifiers;
}
}