diff --git a/src/world/TickingChunkEntry.php b/src/world/TickingChunkEntry.php deleted file mode 100644 index ca965463d..000000000 --- a/src/world/TickingChunkEntry.php +++ /dev/null @@ -1,37 +0,0 @@ - ChunkTicker - * @phpstan-var array - */ - public array $tickers = []; - - public bool $ready = false; -} diff --git a/src/world/World.php b/src/world/World.php index 3a09be176..1803c03f1 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -224,10 +224,25 @@ class World implements ChunkManager{ private array $tickingLoaderCounter = []; /** - * @var TickingChunkEntry[] chunkHash => TickingChunkEntry - * @phpstan-var array + * @var ChunkTicker[][] chunkHash => [spl_object_id => ChunkTicker] + * @phpstan-var array> */ - private array $tickingChunks = []; + private array $registeredTickingChunks = []; + + /** + * Set of chunks which are definitely ready for ticking. + * + * @var int[] + * @phpstan-var array + */ + private array $validTickingChunks = []; + + /** + * Set of chunks which might be ready for ticking. These will be checked at the next tick. + * @var int[] + * @phpstan-var array + */ + private array $recheckTickingChunks = []; /** * @var ChunkLoader[][] chunkHash => [spl_object_id => ChunkLoader] @@ -1157,14 +1172,14 @@ class World implements ChunkManager{ } /** - * Returns a list of chunk position hashes (as returned by World::chunkHash()) which are currently registered for + * Returns a list of chunk position hashes (as returned by World::chunkHash()) which are currently valid for * ticking. * * @return int[] * @phpstan-return list */ public function getTickingChunks() : array{ - return array_keys($this->tickingChunks); + return array_keys($this->validTickingChunks); } /** @@ -1173,11 +1188,8 @@ class World implements ChunkManager{ */ public function registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{ $chunkPosHash = World::chunkHash($chunkX, $chunkZ); - $entry = $this->tickingChunks[$chunkPosHash] ?? null; - if($entry === null){ - $entry = $this->tickingChunks[$chunkPosHash] = new TickingChunkEntry(); - } - $entry->tickers[spl_object_id($ticker)] = $ticker; + $this->registeredTickingChunks[$chunkPosHash][spl_object_id($ticker)] = $ticker; + $this->recheckTickingChunks[$chunkPosHash] = $chunkPosHash; } /** @@ -1187,10 +1199,14 @@ class World implements ChunkManager{ public function unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{ $chunkHash = World::chunkHash($chunkX, $chunkZ); $tickerId = spl_object_id($ticker); - if(isset($this->tickingChunks[$chunkHash]->tickers[$tickerId])){ - unset($this->tickingChunks[$chunkHash]->tickers[$tickerId]); - if(count($this->tickingChunks[$chunkHash]->tickers) === 0){ - unset($this->tickingChunks[$chunkHash]); + if(isset($this->registeredTickingChunks[$chunkHash][$tickerId])){ + unset($this->registeredTickingChunks[$chunkHash][$tickerId]); + if(count($this->registeredTickingChunks[$chunkHash]) === 0){ + unset( + $this->registeredTickingChunks[$chunkHash], + $this->recheckTickingChunks[$chunkHash], + $this->validTickingChunks[$chunkHash] + ); } } } @@ -1234,37 +1250,37 @@ class World implements ChunkManager{ } private function tickChunks() : void{ - if($this->chunkTickRadius <= 0 || (count($this->tickingChunks) === 0 && count($this->tickingLoaders) === 0)){ + if($this->chunkTickRadius <= 0 || (count($this->registeredTickingChunks) === 0 && count($this->tickingLoaders) === 0)){ return; } - $this->timings->randomChunkUpdatesChunkSelection->startTiming(); + if(count($this->recheckTickingChunks) > 0 || count($this->tickingLoaders) > 0){ + $this->timings->randomChunkUpdatesChunkSelection->startTiming(); - /** @var bool[] $chunkTickList chunkhash => dummy */ - $chunkTickList = []; + $chunkTickableCache = []; - $chunkTickableCache = []; - - foreach($this->tickingChunks as $hash => $entry){ - if(!$entry->ready){ + foreach($this->recheckTickingChunks as $hash => $_){ World::getXZ($hash, $chunkX, $chunkZ); if($this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){ - $entry->ready = true; - }else{ - //the chunk has been flagged as temporarily not tickable, so we don't want to tick it this time - continue; + $this->validTickingChunks[$hash] = $hash; } } - $chunkTickList[$hash] = true; - } + $this->recheckTickingChunks = []; - //TODO: REMOVE THIS - //backwards compatibility for TickingChunkLoader, although I'm not sure this is really necessary in practice - if(count($this->tickingLoaders) !== 0){ - $this->selectTickableChunksLegacy($chunkTickList, $chunkTickableCache); - } + //TODO: REMOVE THIS - we need a local var to add extra chunks to if we have legacy ticking loaders + //this is copy-on-write, so it won't have any performance impact if there are no legacy ticking loaders + $chunkTickList = $this->validTickingChunks; - $this->timings->randomChunkUpdatesChunkSelection->stopTiming(); + //TODO: REMOVE THIS + //backwards compatibility for TickingChunkLoader, although I'm not sure this is really necessary in practice + if(count($this->tickingLoaders) !== 0){ + $this->selectTickableChunksLegacy($chunkTickList, $chunkTickableCache); + } + + $this->timings->randomChunkUpdatesChunkSelection->stopTiming(); + }else{ + $chunkTickList = $this->validTickingChunks; + } foreach($chunkTickList as $index => $_){ World::getXZ($index, $chunkX, $chunkZ); @@ -1315,16 +1331,23 @@ class World implements ChunkManager{ } /** - * Marks the 3x3 chunks around the specified chunk as not ready to be ticked. This is used to prevent chunk ticking - * while a chunk is being populated, light-populated, or unloaded. - * Each chunk will be rechecked every tick until it is ready to be ticked again. + * Marks the 3x3 square of chunks centered on the specified chunk for chunk ticking eligibility recheck. + * + * This should be used whenever the chunk's eligibility to be ticked is changed. This includes: + * - Loading/unloading the chunk (the chunk may be registered for ticking before it is loaded) + * - Locking/unlocking the chunk (e.g. world population) + * - Light populated state change (i.e. scheduled for light population, or light population completed) + * - Arbitrary chunk replacement (i.e. setChunk() or similar) */ - private function markTickingChunkUnavailable(int $chunkX, int $chunkZ) : void{ + private function markTickingChunkForRecheck(int $chunkX, int $chunkZ) : void{ for($cx = -1; $cx <= 1; ++$cx){ for($cz = -1; $cz <= 1; ++$cz){ $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz); - if(isset($this->tickingChunks[$chunkHash])){ - $this->tickingChunks[$chunkHash]->ready = false; + unset($this->validTickingChunks[$chunkHash]); + if(isset($this->registeredTickingChunks[$chunkHash])){ + $this->recheckTickingChunks[$chunkHash] = $chunkHash; + }else{ + unset($this->recheckTickingChunks[$chunkHash]); } } } @@ -1335,7 +1358,7 @@ class World implements ChunkManager{ $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated(); if($lightPopulatedState === false){ $this->chunks[$chunkHash]->setLightPopulated(null); - $this->markTickingChunkUnavailable($chunkX, $chunkZ); + $this->markTickingChunkForRecheck($chunkX, $chunkZ); $this->workerPool->submitTask(new LightPopulationTask( $this->chunks[$chunkHash], @@ -1359,6 +1382,7 @@ class World implements ChunkManager{ $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray); } $chunk->setLightPopulated(true); + $this->markTickingChunkForRecheck($chunkX, $chunkZ); } )); } @@ -2403,7 +2427,7 @@ class World implements ChunkManager{ throw new \InvalidArgumentException("Chunk $chunkX $chunkZ is already locked"); } $this->chunkLock[$chunkHash] = $lockId; - $this->markTickingChunkUnavailable($chunkX, $chunkZ); + $this->markTickingChunkForRecheck($chunkX, $chunkZ); } /** @@ -2418,6 +2442,7 @@ class World implements ChunkManager{ $chunkHash = World::chunkHash($chunkX, $chunkZ); if(isset($this->chunkLock[$chunkHash]) && ($lockId === null || $this->chunkLock[$chunkHash] === $lockId)){ unset($this->chunkLock[$chunkHash]); + $this->markTickingChunkForRecheck($chunkX, $chunkZ); return true; } return false; @@ -2469,7 +2494,7 @@ class World implements ChunkManager{ unset($this->blockCache[$chunkHash]); unset($this->changedBlocks[$chunkHash]); $chunk->setTerrainDirty(); - $this->markTickingChunkUnavailable($chunkX, $chunkZ); //this replacement chunk may not meet the conditions for ticking + $this->markTickingChunkForRecheck($chunkX, $chunkZ); //this replacement chunk may not meet the conditions for ticking if(!$this->isChunkInUse($chunkX, $chunkZ)){ $this->unloadChunkRequest($chunkX, $chunkZ); @@ -2751,6 +2776,7 @@ class World implements ChunkManager{ foreach($this->getChunkListeners($x, $z) as $listener){ $listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]); } + $this->markTickingChunkForRecheck($x, $z); //tickers may have been registered before the chunk was loaded $this->timings->syncChunkLoad->stopTiming(); @@ -2912,8 +2938,8 @@ class World implements ChunkManager{ unset($this->chunks[$chunkHash]); unset($this->blockCache[$chunkHash]); unset($this->changedBlocks[$chunkHash]); - unset($this->tickingChunks[$chunkHash]); - $this->markTickingChunkUnavailable($x, $z); + unset($this->registeredTickingChunks[$chunkHash]); + $this->markTickingChunkForRecheck($x, $z); if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){ $this->logger->debug("Rejecting population promise for chunk $x $z");