World: Track entities separately from chunks

this allows entities to exist outside of generated chunks, with one caveat: they won't be saved in such cases.
Obviously, for player entities, this doesn't matter.

fixes #3947
This commit is contained in:
Dylan K. Taylor
2021-09-09 01:17:41 +01:00
parent ba2bfe0e11
commit 34f01a3ce3
4 changed files with 67 additions and 102 deletions

View File

@ -93,6 +93,7 @@ use pocketmine\world\sound\Sound;
use pocketmine\world\utils\SubChunkExplorer;
use function abs;
use function array_fill_keys;
use function array_filter;
use function array_key_exists;
use function array_map;
use function array_merge;
@ -158,6 +159,12 @@ class World implements ChunkManager{
*/
private $entityLastKnownPositions = [];
/**
* @var Entity[][]
* @phpstan-var array<int, array<int, Entity>>
*/
private array $entitiesByChunk = [];
/** @var Entity[] */
public $updateEntities = [];
/** @var Block[][] */
@ -516,6 +523,15 @@ class World implements ChunkManager{
self::getXZ($chunkHash, $chunkX, $chunkZ);
$this->unloadChunk($chunkX, $chunkZ, false);
}
foreach($this->entitiesByChunk as $chunkHash => $entities){
self::getXZ($chunkHash, $chunkX, $chunkZ);
if(count($entities) !== 0){
$this->logger->warning(count($entities) . " entities found in ungenerated chunk $chunkX $chunkZ, they won't be saved!");
}
foreach($entities as $entity){
$entity->close();
}
}
$this->save();
@ -1070,7 +1086,7 @@ class World implements ChunkManager{
if($chunk === null){
throw new \InvalidArgumentException("Chunk is not loaded");
}
foreach($chunk->getEntities() as $entity){
foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){
$entity->onRandomUpdate();
}
@ -1129,7 +1145,7 @@ class World implements ChunkManager{
self::getXZ($chunkHash, $chunkX, $chunkZ);
$this->provider->saveChunk($chunkX, $chunkZ, new ChunkData(
$chunk,
array_map(fn(Entity $e) => $e->saveNBT(), $chunk->getSavableEntities()),
array_map(fn(Entity $e) => $e->saveNBT(), array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk())),
array_map(fn(Tile $t) => $t->saveNBT(), $chunk->getTiles()),
));
$chunk->clearTerrainDirtyFlags();
@ -1921,7 +1937,7 @@ class World implements ChunkManager{
if(!$this->isChunkLoaded($x, $z)){
continue;
}
foreach($this->getChunk($x, $z)->getEntities() as $ent){
foreach($this->getChunkEntities($x, $z) as $ent){
if($ent !== $entity and $ent->boundingBox->intersectsWith($bb)){
$nearby[] = $ent;
}
@ -1964,7 +1980,7 @@ class World implements ChunkManager{
if(!$this->isChunkLoaded($x, $z)){
continue;
}
foreach($this->getChunk($x, $z)->getEntities() as $entity){
foreach($this->getChunkEntities($x, $z) as $entity){
if(!($entity instanceof $entityType) or $entity->isFlaggedForDespawn() or (!$includeDead and !$entity->isAlive())){
continue;
}
@ -2043,6 +2059,13 @@ class World implements ChunkManager{
return $this->chunks[World::chunkHash($chunkX, $chunkZ)] ?? null;
}
/**
* @return Entity[]
*/
public function getChunkEntities(int $chunkX, int $chunkZ) : array{
return $this->entitiesByChunk[World::chunkHash($chunkX, $chunkZ)] ?? [];
}
/**
* Returns the chunk containing the given Vector3 position.
*/
@ -2154,11 +2177,8 @@ class World implements ChunkManager{
$oldChunk = $this->loadChunk($chunkX, $chunkZ);
if($oldChunk !== null and $oldChunk !== $chunk){
if($deleteEntitiesAndTiles){
foreach($oldChunk->getEntities() as $entity){
if($entity instanceof Player){
$chunk->addEntity($entity);
$oldChunk->removeEntity($entity);
}else{
foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){
if(!($entity instanceof Player)){
$entity->close();
}
}
@ -2166,11 +2186,6 @@ class World implements ChunkManager{
$tile->close();
}
}else{
foreach($oldChunk->getEntities() as $entity){
$chunk->addEntity($entity);
$oldChunk->removeEntity($entity);
}
foreach($oldChunk->getTiles() as $tile){
$chunk->addTile($tile);
$oldChunk->removeTile($tile);
@ -2272,7 +2287,7 @@ class World implements ChunkManager{
if($chunk === null){
throw new \InvalidArgumentException("Cannot add an Entity in an ungenerated chunk");
}
$chunk->addEntity($entity);
$this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> 4, $pos->getFloorZ() >> 4)][$entity->getId()] = $entity;
$this->entityLastKnownPositions[$entity->getId()] = $pos;
if($entity instanceof Player){
@ -2294,9 +2309,12 @@ class World implements ChunkManager{
throw new \InvalidArgumentException("Entity is not tracked by this world (possibly already removed?)");
}
$pos = $this->entityLastKnownPositions[$entity->getId()];
$chunk = $this->getChunk($pos->getFloorX() >> 4, $pos->getFloorZ() >> 4);
if($chunk !== null){ //we don't care if the chunk already went out of scope
$chunk->removeEntity($entity);
$chunkHash = World::chunkHash($pos->getFloorX() >> 4, $pos->getFloorZ() >> 4);
if(isset($this->entitiesByChunk[$chunkHash][$entity->getId()])){
unset($this->entitiesByChunk[$chunkHash][$entity->getId()]);
if(count($this->entitiesByChunk[$chunkHash]) === 0){
unset($this->entitiesByChunk[$chunkHash]);
}
}
unset($this->entityLastKnownPositions[$entity->getId()]);
@ -2326,35 +2344,28 @@ class World implements ChunkManager{
$newChunkZ = $newPosition->getFloorZ() >> 4;
if($oldChunkX !== $newChunkX || $oldChunkZ !== $newChunkZ){
$oldChunk = $this->getChunk($oldChunkX, $oldChunkZ);
if($oldChunk !== null){
$oldChunk->removeEntity($entity);
$oldChunkHash = World::chunkHash($oldChunkX, $oldChunkZ);
if(isset($this->entitiesByChunk[$oldChunkHash][$entity->getId()])){
unset($this->entitiesByChunk[$oldChunkHash][$entity->getId()]);
if(count($this->entitiesByChunk[$oldChunkHash]) === 0){
unset($this->entitiesByChunk[$oldChunkHash]);
}
}
$newChunk = $this->loadChunk($newChunkX, $newChunkZ);
if($newChunk === null){
//TODO: this is a non-ideal solution for a hard problem
//when this happens the entity won't be tracked by any chunk, so we can't have it hanging around in memory
//we also can't allow this to cause chunk generation, nor can we just create an empty ungenerated chunk
//for it, because an empty chunk won't get saved, so the entity will vanish anyway. Therefore, this is
//the cleanest way to make sure this doesn't result in leaks.
$this->logger->debug("Entity " . $entity->getId() . " is in ungenerated terrain, flagging for despawn");
$entity->flagForDespawn();
$entity->despawnFromAll();
}else{
$newViewers = $this->getViewersForPosition($newPosition);
foreach($entity->getViewers() as $player){
if(!isset($newViewers[spl_object_id($player)])){
$entity->despawnFrom($player);
}else{
unset($newViewers[spl_object_id($player)]);
}
}
foreach($newViewers as $player){
$entity->spawnTo($player);
}
$newChunk->addEntity($entity);
$newViewers = $this->getViewersForPosition($newPosition);
foreach($entity->getViewers() as $player){
if(!isset($newViewers[spl_object_id($player)])){
$entity->despawnFrom($player);
}else{
unset($newViewers[spl_object_id($player)]);
}
}
foreach($newViewers as $player){
$entity->spawnTo($player);
}
$newChunkHash = World::chunkHash($newChunkX, $newChunkZ);
$this->entitiesByChunk[$newChunkHash][$entity->getId()] = $entity;
}
$this->entityLastKnownPositions[$entity->getId()] = $newPosition->asVector3();
}
@ -2564,7 +2575,7 @@ class World implements ChunkManager{
try{
$this->provider->saveChunk($x, $z, new ChunkData(
$chunk,
array_map(fn(Entity $e) => $e->saveNBT(), $chunk->getSavableEntities()),
array_map(fn(Entity $e) => $e->saveNBT(), array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk())),
array_map(fn(Tile $t) => $t->saveNBT(), $chunk->getTiles()),
));
}finally{
@ -2576,6 +2587,13 @@ class World implements ChunkManager{
$listener->onChunkUnloaded($x, $z, $chunk);
}
foreach($this->getChunkEntities($x, $z) as $entity){
if($entity instanceof Player){
continue;
}
$entity->close();
}
$chunk->onUnload();
}