153    private static int $worldIdCounter = 1;
 
  155    public const Y_MAX = 320;
 
  156    public const Y_MIN = -64;
 
  158    public const TIME_DAY = 1000;
 
  159    public const TIME_NOON = 6000;
 
  160    public const TIME_SUNSET = 12000;
 
  161    public const TIME_NIGHT = 13000;
 
  162    public const TIME_MIDNIGHT = 18000;
 
  163    public const TIME_SUNRISE = 23000;
 
  165    public const TIME_FULL = 24000;
 
  167    public const DIFFICULTY_PEACEFUL = 0;
 
  168    public const DIFFICULTY_EASY = 1;
 
  169    public const DIFFICULTY_NORMAL = 2;
 
  170    public const DIFFICULTY_HARD = 3;
 
  172    public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3;
 
  175    private const BLOCK_CACHE_SIZE_CAP = 2048;
 
  181    private array $players = [];
 
  187    private array $entities = [];
 
  192    private array $entityLastKnownPositions = [];
 
  198    private array $entitiesByChunk = [];
 
  204    public array $updateEntities = [];
 
  206    private bool $inDynamicStateRecalculation = 
false;
 
  211    private array $blockCache = [];
 
  212    private int $blockCacheSize = 0;
 
  217    private array $blockCollisionBoxCache = [];
 
  219    private int $sendTimeTicker = 0;
 
  221    private int $worldId;
 
  223    private int $providerGarbageCollectionTicker = 0;
 
  232    private array $registeredTickingChunks = [];
 
  240    private array $validTickingChunks = [];
 
  247    private array $recheckTickingChunks = [];
 
  253    private array $chunkLoaders = [];
 
  259    private array $chunkListeners = [];
 
  264    private array $playerChunkListeners = [];
 
  270    private array $packetBuffersByChunk = [];
 
  276    private array $unloadQueue = [];
 
  279    public bool $stopTime = 
false;
 
  281    private float $sunAnglePercentage = 0.0;
 
  282    private int $skyLightReduction = 0;
 
  284    private string $folderName;
 
  285    private string $displayName;
 
  291    private array $chunks = [];
 
  297    private array $knownUngeneratedChunks = [];
 
  303    private array $changedBlocks = [];
 
  311    private array $scheduledBlockUpdateQueueIndex = [];
 
  314    private \SplQueue $neighbourBlockUpdateQueue;
 
  319    private array $neighbourBlockUpdateQueueIndex = [];
 
  325    private array $activeChunkPopulationTasks = [];
 
  330    private array $chunkLock = [];
 
  331    private int $maxConcurrentChunkPopulationTasks = 2;
 
  336    private array $chunkPopulationRequestMap = [];
 
  341    private \SplQueue $chunkPopulationRequestQueue;
 
  346    private array $chunkPopulationRequestQueueIndex = [];
 
  350    private bool $autoSave = 
true;
 
  352    private int $sleepTicks = 0;
 
  354    private int $chunkTickRadius;
 
  355    private int $tickedBlocksPerSubchunkPerTick = self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK;
 
  360    private array $randomTickBlocks = [];
 
  364    public float $tickRateTime = 0;
 
  366    private bool $doingTick = 
false;
 
  368    private bool $unloaded = 
false;
 
  373    private array $unloadCallbacks = [];
 
  378    private \Logger $logger;
 
  386        return morton2d_encode($x, $z);
 
 
  389    private const MORTON3D_BIT_SIZE = 21;
 
  390    private const BLOCKHASH_Y_BITS = 9;
 
  391    private const BLOCKHASH_Y_PADDING = 64; 
 
  392    private const BLOCKHASH_Y_OFFSET = self::BLOCKHASH_Y_PADDING - self::Y_MIN;
 
  393    private const BLOCKHASH_Y_MASK = (1 << self::BLOCKHASH_Y_BITS) - 1;
 
  394    private const BLOCKHASH_XZ_MASK = (1 << self::MORTON3D_BIT_SIZE) - 1;
 
  395    private const BLOCKHASH_XZ_EXTRA_BITS = (self::MORTON3D_BIT_SIZE - self::BLOCKHASH_Y_BITS) >> 1;
 
  396    private const BLOCKHASH_XZ_EXTRA_MASK = (1 << self::BLOCKHASH_XZ_EXTRA_BITS) - 1;
 
  397    private const BLOCKHASH_XZ_SIGN_SHIFT = 64 - self::MORTON3D_BIT_SIZE - self::BLOCKHASH_XZ_EXTRA_BITS;
 
  398    private const BLOCKHASH_X_SHIFT = self::BLOCKHASH_Y_BITS;
 
  399    private const BLOCKHASH_Z_SHIFT = self::BLOCKHASH_X_SHIFT + self::BLOCKHASH_XZ_EXTRA_BITS;
 
  404    public static function blockHash(
int $x, 
int $y, 
int $z) : int{
 
  405        $shiftedY = $y + self::BLOCKHASH_Y_OFFSET;
 
  406        if(($shiftedY & (~0 << self::BLOCKHASH_Y_BITS)) !== 0){
 
  407            throw new \InvalidArgumentException(
"Y coordinate $y is out of range!");
 
  412        return morton3d_encode(
 
  413            $x & self::BLOCKHASH_XZ_MASK,
 
  415                ((($x >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_X_SHIFT) |
 
  416                ((($z >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_Z_SHIFT),
 
  417            $z & self::BLOCKHASH_XZ_MASK
 
 
  425        return morton3d_encode($x, $y, $z);
 
 
  434    public static function getBlockXYZ(
int $hash, ?
int &$x, ?
int &$y, ?
int &$z) : void{
 
  435        [$baseX, $baseY, $baseZ] = morton3d_decode($hash);
 
  437        $extraX = ((($baseY >> self::BLOCKHASH_X_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
 
  438        $extraZ = ((($baseY >> self::BLOCKHASH_Z_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
 
  440        $x = (($baseX & self::BLOCKHASH_XZ_MASK) | $extraX) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
 
  441        $y = ($baseY & self::BLOCKHASH_Y_MASK) - self::BLOCKHASH_Y_OFFSET;
 
  442        $z = (($baseZ & self::BLOCKHASH_XZ_MASK) | $extraZ) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
 
 
  450    public static function getXZ(
int $hash, ?
int &$x, ?
int &$z) : void{
 
  451        [$x, $z] = morton2d_decode($hash);
 
 
  454    public static function getDifficultyFromString(
string $str) : int{
 
  455        switch(strtolower(trim($str))){
 
  459                return World::DIFFICULTY_PEACEFUL;
 
  464                return World::DIFFICULTY_EASY;
 
  469                return World::DIFFICULTY_NORMAL;
 
  474                return World::DIFFICULTY_HARD;
 
  489        $this->folderName = $name;
 
  490        $this->worldId = self::$worldIdCounter++;
 
  492        $this->displayName = $this->provider->getWorldData()->getName();
 
  493        $this->logger = new \PrefixedLogger($server->getLogger(), 
"World: $this->displayName");
 
  495        $this->blockStateRegistry = RuntimeBlockStateRegistry::getInstance();
 
  496        $this->minY = $this->provider->getWorldMinY();
 
  497        $this->maxY = $this->provider->getWorldMaxY();
 
  499        $this->
server->getLogger()->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_preparing($this->displayName)));
 
  500        $generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
 
  501            throw new AssumptionFailedError(
"WorldManager should already have checked that the generator exists");
 
  502        $generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
 
  505            worldMinY: $this->minY,
 
  506            worldMaxY: $this->maxY,
 
  507            generatorSeed: $this->getSeed(),
 
  508            generatorClass: $generator->getGeneratorClass(),
 
  509            generatorSettings: $this->provider->getWorldData()->getGeneratorOptions()
 
  511        $this->generatorExecutor = $generator->isFast() ?
 
  516                $executorSetupParameters,
 
  520        $this->chunkPopulationRequestQueue = new \SplQueue();
 
  521        $this->addOnUnloadCallback(
function() : 
void{
 
  522            $this->logger->debug(
"Cancelling unfulfilled generation requests");
 
  524            foreach($this->chunkPopulationRequestMap as $chunkHash => $promise){
 
  526                unset($this->chunkPopulationRequestMap[$chunkHash]);
 
  528            if(count($this->chunkPopulationRequestMap) !== 0){
 
  536        $this->scheduledBlockUpdateQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
 
  538        $this->neighbourBlockUpdateQueue = new \SplQueue();
 
  540        $this->time = $this->provider->getWorldData()->getTime();
 
  542        $cfg = $this->
server->getConfigGroup();
 
  543        $this->chunkTickRadius = min($this->
server->getViewDistance(), max(0, $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_TICK_RADIUS, 4)));
 
  544        if($cfg->getPropertyInt(
"chunk-ticking.per-tick", 40) <= 0){
 
  546            $this->logger->warning(
"\"chunk-ticking.per-tick\" setting is deprecated, but you've used it to disable chunk ticking. Set \"chunk-ticking.tick-radius\" to 0 in \"pocketmine.yml\" instead.");
 
  547            $this->chunkTickRadius = 0;
 
  549        $this->tickedBlocksPerSubchunkPerTick = $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_BLOCKS_PER_SUBCHUNK_PER_TICK, self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK);
 
  550        $this->maxConcurrentChunkPopulationTasks = $cfg->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2);
 
  552        $this->initRandomTickBlocksFromConfig($cfg);
 
 
  558        $dontTickBlocks = [];
 
  559        $parser = StringToItemParser::getInstance();
 
  560        foreach($cfg->getProperty(YmlServerProperties::CHUNK_TICKING_DISABLE_BLOCK_TICKING, []) as $name){
 
  561            $name = (string) $name;
 
  562            $item = $parser->parse($name);
 
  564                $block = $item->getBlock();
 
  565            }elseif(preg_match(
"/^-?\d+$/", $name) === 1){
 
  568                    $blockStateData = GlobalBlockStateHandlers::getUpgrader()->upgradeIntIdMeta((
int) $name, 0);
 
  569                }
catch(BlockStateDeserializeException){
 
  572                $block = $this->blockStateRegistry->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData));
 
  578            if($block->getTypeId() !== BlockTypeIds::AIR){
 
  579                $dontTickBlocks[$block->getTypeId()] = $name;
 
  583        foreach($this->blockStateRegistry->getAllKnownStates() as $state){
 
  584            $dontTickName = $dontTickBlocks[$state->getTypeId()] ?? 
null;
 
  585            if($dontTickName === 
null && $state->ticksRandomly()){
 
  586                $this->randomTickBlocks[$state->getStateId()] = 
true;
 
  591    public function getTickRateTime() : float{
 
  592        return $this->tickRateTime;
 
  595    public function getServer() : Server{
 
  599    public function getLogger() : \
Logger{
 
  600        return $this->logger;
 
  603    final public function getProvider() : WritableWorldProvider{
 
  604        return $this->provider;
 
  610    final public function getId() : int{
 
  611        return $this->worldId;
 
 
  614    public function isLoaded() : bool{
 
  615        return !$this->unloaded;
 
  621    public function onUnload() : void{
 
  623            throw new \LogicException(
"Tried to close a world which is already closed");
 
  626        foreach($this->unloadCallbacks as $callback){
 
  629        $this->unloadCallbacks = [];
 
  631        foreach($this->chunks as $chunkHash => $chunk){
 
  632            self::getXZ($chunkHash, $chunkX, $chunkZ);
 
  633            $this->unloadChunk($chunkX, $chunkZ, 
false);
 
  635        $this->knownUngeneratedChunks = [];
 
  636        foreach($this->entitiesByChunk as $chunkHash => $entities){
 
  637            self::getXZ($chunkHash, $chunkX, $chunkZ);
 
  640            foreach($entities as $entity){
 
  641                if(!$entity->isFlaggedForDespawn()){
 
  646            if($leakedEntities !== 0){
 
  647                $this->logger->warning(
"$leakedEntities leaked entities found in ungenerated chunk $chunkX $chunkZ during unload, they won't be saved!");
 
  653        $this->generatorExecutor->shutdown();
 
  655        $this->provider->close();
 
  656        $this->blockCache = [];
 
  657        $this->blockCacheSize = 0;
 
  658        $this->blockCollisionBoxCache = [];
 
  660        $this->unloaded = 
true;
 
  665        $this->unloadCallbacks[spl_object_id($callback)] = $callback;
 
 
  670        unset($this->unloadCallbacks[spl_object_id($callback)]);
 
 
  681    private function filterViewersForPosition(
Vector3 $pos, array $allowed) : array{
 
  682        $candidates = $this->getViewersForPosition($pos);
 
  684        foreach($allowed as $player){
 
  685            $k = spl_object_id($player);
 
  686            if(isset($candidates[$k])){
 
  687                $filtered[$k] = $candidates[$k];
 
  698        $players ??= $this->getViewersForPosition($pos);
 
  700        if(WorldSoundEvent::hasHandlers()){
 
  703            if($ev->isCancelled()){
 
  707            $sound = $ev->getSound();
 
  708            $players = $ev->getRecipients();
 
  711        $pk = $sound->
encode($pos);
 
  713            if($players === $this->getViewersForPosition($pos)){
 
  715                    $this->broadcastPacketToViewers($pos, $e);
 
  718                NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
 
 
  727        $players ??= $this->getViewersForPosition($pos);
 
  729        if(WorldParticleEvent::hasHandlers()){
 
  732            if($ev->isCancelled()){
 
  736            $particle = $ev->getParticle();
 
  737            $players = $ev->getRecipients();
 
  740        $pk = $particle->
encode($pos);
 
  742            if($players === $this->getViewersForPosition($pos)){
 
  744                    $this->broadcastPacketToViewers($pos, $e);
 
  747                NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
 
 
  752    public function getAutoSave() : bool{
 
  753        return $this->autoSave;
 
  756    public function setAutoSave(
bool $value) : void{
 
  757        $this->autoSave = $value;
 
  770        return $this->playerChunkListeners[
World::chunkHash($chunkX, $chunkZ)] ?? [];
 
 
  780        return $this->chunkLoaders[
World::chunkHash($chunkX, $chunkZ)] ?? [];
 
 
  790        return $this->getChunkPlayers($pos->getFloorX() >> 
Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> 
Chunk::COORD_BIT_SIZE);
 
 
  797        $this->broadcastPacketToPlayersUsingChunk($pos->getFloorX() >> 
Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> 
Chunk::COORD_BIT_SIZE, $packet);
 
 
  800    private function broadcastPacketToPlayersUsingChunk(
int $chunkX, 
int $chunkZ, 
ClientboundPacket $packet) : void{
 
  801        if(!isset($this->packetBuffersByChunk[$index = 
World::chunkHash($chunkX, $chunkZ)])){
 
  802            $this->packetBuffersByChunk[$index] = [$packet];
 
  804            $this->packetBuffersByChunk[$index][] = $packet;
 
  808    public function registerChunkLoader(ChunkLoader $loader, 
int $chunkX, 
int $chunkZ, 
bool $autoLoad = 
true) : void{
 
  809        $loaderId = spl_object_id($loader);
 
  811        if(!isset($this->chunkLoaders[$chunkHash = World::chunkHash($chunkX, $chunkZ)])){
 
  812            $this->chunkLoaders[$chunkHash] = [];
 
  813        }elseif(isset($this->chunkLoaders[$chunkHash][$loaderId])){
 
  817        $this->chunkLoaders[$chunkHash][$loaderId] = $loader;
 
  819        $this->cancelUnloadChunkRequest($chunkX, $chunkZ);
 
  822            $this->loadChunk($chunkX, $chunkZ);
 
  826    public function unregisterChunkLoader(ChunkLoader $loader, 
int $chunkX, 
int $chunkZ) : void{
 
  827        $chunkHash = World::chunkHash($chunkX, $chunkZ);
 
  828        $loaderId = spl_object_id($loader);
 
  829        if(isset($this->chunkLoaders[$chunkHash][$loaderId])){
 
  830            if(count($this->chunkLoaders[$chunkHash]) === 1){
 
  831                unset($this->chunkLoaders[$chunkHash]);
 
  832                $this->unloadChunkRequest($chunkX, $chunkZ, 
true);
 
  833                if(isset($this->chunkPopulationRequestMap[$chunkHash]) && !isset($this->activeChunkPopulationTasks[$chunkHash])){
 
  834                    $this->chunkPopulationRequestMap[$chunkHash]->reject();
 
  835                    unset($this->chunkPopulationRequestMap[$chunkHash]);
 
  838                unset($this->chunkLoaders[$chunkHash][$loaderId]);
 
  847        $hash = 
World::chunkHash($chunkX, $chunkZ);
 
  848        if(isset($this->chunkListeners[$hash])){
 
  849            $this->chunkListeners[$hash][spl_object_id($listener)] = $listener;
 
  851            $this->chunkListeners[$hash] = [spl_object_id($listener) => $listener];
 
  853        if($listener instanceof 
Player){
 
  854            $this->playerChunkListeners[$hash][spl_object_id($listener)] = $listener;
 
 
  864        $hash = 
World::chunkHash($chunkX, $chunkZ);
 
  865        if(isset($this->chunkListeners[$hash])){
 
  866            if(count($this->chunkListeners[$hash]) === 1){
 
  867                unset($this->chunkListeners[$hash]);
 
  868                unset($this->playerChunkListeners[$hash]);
 
  870                unset($this->chunkListeners[$hash][spl_object_id($listener)]);
 
  871                unset($this->playerChunkListeners[$hash][spl_object_id($listener)]);
 
 
  880        foreach($this->chunkListeners as $hash => $listeners){
 
  881            World::getXZ($hash, $chunkX, $chunkZ);
 
  882            $this->unregisterChunkListener($listener, $chunkX, $chunkZ);
 
 
  893        return $this->chunkListeners[
World::chunkHash($chunkX, $chunkZ)] ?? [];
 
 
  899    public function sendTime(
Player ...$targets) : void{
 
  900        if(count($targets) === 0){
 
  901            $targets = $this->players;
 
  903        foreach($targets as $player){
 
  904            $player->getNetworkSession()->syncWorldTime($this->time);
 
  908    public function isDoingTick() : bool{
 
  909        return $this->doingTick;
 
  915    public function doTick(
int $currentTick) : void{
 
  917            throw new \LogicException(
"Attempted to tick a world which has been closed");
 
  920        $this->timings->doTick->startTiming();
 
  921        $this->doingTick = 
true;
 
  923            $this->actuallyDoTick($currentTick);
 
  925            $this->doingTick = 
false;
 
  926            $this->timings->doTick->stopTiming();
 
  930    protected function actuallyDoTick(
int $currentTick) : void{
 
  931        if(!$this->stopTime){
 
  933            if($this->time === PHP_INT_MAX){
 
  934                $this->time = PHP_INT_MIN;
 
  940        $this->sunAnglePercentage = $this->computeSunAnglePercentage(); 
 
  941        $this->skyLightReduction = $this->computeSkyLightReduction(); 
 
  943        if(++$this->sendTimeTicker === 200){
 
  945            $this->sendTimeTicker = 0;
 
  948        $this->unloadChunks();
 
  949        if(++$this->providerGarbageCollectionTicker >= 6000){
 
  950            $this->provider->doGarbageCollection();
 
  951            $this->providerGarbageCollectionTicker = 0;
 
  954        $this->timings->scheduledBlockUpdates->startTiming();
 
  956        while($this->scheduledBlockUpdateQueue->count() > 0 && $this->scheduledBlockUpdateQueue->current()[
"priority"] <= $currentTick){
 
  958            $vec = $this->scheduledBlockUpdateQueue->extract()[
"data"];
 
  959            unset($this->scheduledBlockUpdateQueueIndex[World::blockHash($vec->x, $vec->y, $vec->z)]);
 
  960            if(!$this->isInLoadedTerrain($vec)){
 
  963            $block = $this->getBlock($vec);
 
  964            $block->onScheduledUpdate();
 
  966        $this->timings->scheduledBlockUpdates->stopTiming();
 
  968        $this->timings->neighbourBlockUpdates->startTiming();
 
  970        while($this->neighbourBlockUpdateQueue->count() > 0){
 
  971            $index = $this->neighbourBlockUpdateQueue->dequeue();
 
  972            unset($this->neighbourBlockUpdateQueueIndex[$index]);
 
  973            World::getBlockXYZ($index, $x, $y, $z);
 
  974            if(!$this->isChunkLoaded($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)){
 
  978            $block = $this->getBlockAt($x, $y, $z);
 
  980            if(BlockUpdateEvent::hasHandlers()){
 
  981                $ev = 
new BlockUpdateEvent($block);
 
  983                if($ev->isCancelled()){
 
  987            foreach($this->getNearbyEntities(AxisAlignedBB::one()->offset($x, $y, $z)) as $entity){
 
  988                $entity->onNearbyBlockChange();
 
  990            $block->onNearbyBlockChange();
 
  993        $this->timings->neighbourBlockUpdates->stopTiming();
 
  995        $this->timings->entityTick->startTiming();
 
  997        foreach($this->updateEntities as $id => $entity){
 
  998            if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->onUpdate($currentTick)){
 
  999                unset($this->updateEntities[$id]);
 
 1001            if($entity->isFlaggedForDespawn()){
 
 1005        $this->timings->entityTick->stopTiming();
 
 1007        $this->timings->randomChunkUpdates->startTiming();
 
 1008        $this->tickChunks();
 
 1009        $this->timings->randomChunkUpdates->stopTiming();
 
 1011        $this->executeQueuedLightUpdates();
 
 1013        if(count($this->changedBlocks) > 0){
 
 1014            if(count($this->players) > 0){
 
 1015                foreach($this->changedBlocks as $index => $blocks){
 
 1016                    if(count($blocks) === 0){ 
 
 1019                    World::getXZ($index, $chunkX, $chunkZ);
 
 1020                    if(!$this->isChunkLoaded($chunkX, $chunkZ)){
 
 1024                    if(count($blocks) > 512){
 
 1025                        $chunk = $this->getChunk($chunkX, $chunkZ) ?? 
throw new AssumptionFailedError(
"We already checked that the chunk is loaded");
 
 1026                        foreach($this->getChunkPlayers($chunkX, $chunkZ) as $p){
 
 1027                            $p->onChunkChanged($chunkX, $chunkZ, $chunk);
 
 1030                        foreach($this->createBlockUpdatePackets($blocks) as $packet){
 
 1031                            $this->broadcastPacketToPlayersUsingChunk($chunkX, $chunkZ, $packet);
 
 1037            $this->changedBlocks = [];
 
 1041        if($this->sleepTicks > 0 && --$this->sleepTicks <= 0){
 
 1042            $this->checkSleep();
 
 1045        foreach($this->packetBuffersByChunk as $index => $entries){
 
 1046            World::getXZ($index, $chunkX, $chunkZ);
 
 1047            $chunkPlayers = $this->getChunkPlayers($chunkX, $chunkZ);
 
 1048            if(count($chunkPlayers) > 0){
 
 1049                NetworkBroadcastUtils::broadcastPackets($chunkPlayers, $entries);
 
 1053        $this->packetBuffersByChunk = [];
 
 1056    public function checkSleep() : void{
 
 1057        if(count($this->players) === 0){
 
 1062        foreach($this->getPlayers() as $p){
 
 1063            if(!$p->isSleeping()){
 
 1070            $time = $this->getTimeOfDay();
 
 1072            if($time >= World::TIME_NIGHT && $time < World::TIME_SUNRISE){
 
 1073                $this->setTime($this->getTime() + World::TIME_FULL - $time);
 
 1075                foreach($this->getPlayers() as $p){
 
 1082    public function setSleepTicks(
int $ticks) : void{
 
 1083        $this->sleepTicks = $ticks;
 
 1095        $blockTranslator = TypeConverter::getInstance()->getBlockTranslator();
 
 1097        foreach($blocks as $b){
 
 1099                throw new \TypeError(
"Expected Vector3 in blocks array, got " . (is_object($b) ? get_class($b) : gettype($b)));
 
 1102            $fullBlock = $this->getBlockAt($b->x, $b->y, $b->z);
 
 1103            $blockPosition = BlockPosition::fromVector3($b);
 
 1105            $tile = $this->getTileAt($b->x, $b->y, $b->z);
 
 1107                $expectedClass = $fullBlock->getIdInfo()->getTileClass();
 
 1108                if($expectedClass !== 
null && $tile instanceof $expectedClass && count($fakeStateProperties = $tile->getRenderUpdateBugWorkaroundStateProperties($fullBlock)) > 0){
 
 1109                    $originalStateData = $blockTranslator->internalIdToNetworkStateData($fullBlock->getStateId());
 
 1111                        $originalStateData->getName(),
 
 1112                        array_merge($originalStateData->getStates(), $fakeStateProperties),
 
 1113                        $originalStateData->getVersion()
 
 1115                    $packets[] = UpdateBlockPacket::create(
 
 1117                        $blockTranslator->getBlockStateDictionary()->lookupStateIdFromData($fakeStateData) ?? 
throw new AssumptionFailedError(
"Unmapped fake blockstate data: " . $fakeStateData->toNbt()),
 
 1118                        UpdateBlockPacket::FLAG_NETWORK,
 
 1119                        UpdateBlockPacket::DATA_LAYER_NORMAL
 
 1123            $packets[] = UpdateBlockPacket::create(
 
 1125                $blockTranslator->internalIdToNetworkId($fullBlock->getStateId()),
 
 1126                UpdateBlockPacket::FLAG_NETWORK,
 
 1127                UpdateBlockPacket::DATA_LAYER_NORMAL
 
 1131                $packets[] = BlockActorDataPacket::create($blockPosition, $tile->getSerializedSpawnCompound());
 
 
 1138    public function clearCache(
bool $force = 
false) : void{
 
 1140            $this->blockCache = [];
 
 1141            $this->blockCacheSize = 0;
 
 1142            $this->blockCollisionBoxCache = [];
 
 1145            $this->blockCacheSize = 0;
 
 1146            foreach($this->blockCache as $list){
 
 1147                $this->blockCacheSize += count($list);
 
 1148                if($this->blockCacheSize > self::BLOCK_CACHE_SIZE_CAP){
 
 1149                    $this->blockCache = [];
 
 1150                    $this->blockCacheSize = 0;
 
 1156            foreach($this->blockCollisionBoxCache as $list){
 
 1157                $count += count($list);
 
 1158                if($count > self::BLOCK_CACHE_SIZE_CAP){
 
 1160                    $this->blockCollisionBoxCache = [];
 
 1167    private function trimBlockCache() : void{
 
 1168        $before = $this->blockCacheSize;
 
 1171        foreach($this->blockCache as $chunkHash => $blocks){
 
 1172            unset($this->blockCache[$chunkHash]);
 
 1173            $this->blockCacheSize -= count($blocks);
 
 1174            if($this->blockCacheSize < self::BLOCK_CACHE_SIZE_CAP){
 
 1185        return $this->randomTickBlocks;
 
 
 1188    public function addRandomTickedBlock(
Block $block) : void{
 
 1190            throw new \InvalidArgumentException(
"Cannot do random-tick on unknown block");
 
 1192        $this->randomTickBlocks[$block->getStateId()] = 
true;
 
 1195    public function removeRandomTickedBlock(Block $block) : void{
 
 1196        unset($this->randomTickBlocks[$block->getStateId()]);
 
 1204        return $this->chunkTickRadius;
 
 
 1212        $this->chunkTickRadius = $radius;
 
 
 1223        return array_keys($this->validTickingChunks);
 
 
 1231        $chunkPosHash = 
World::chunkHash($chunkX, $chunkZ);
 
 1232        $this->registeredTickingChunks[$chunkPosHash][spl_object_id($ticker)] = $ticker;
 
 1233        $this->recheckTickingChunks[$chunkPosHash] = $chunkPosHash;
 
 
 1241        $chunkHash = 
World::chunkHash($chunkX, $chunkZ);
 
 1242        $tickerId = spl_object_id($ticker);
 
 1243        if(isset($this->registeredTickingChunks[$chunkHash][$tickerId])){
 
 1244            if(count($this->registeredTickingChunks[$chunkHash]) === 1){
 
 1246                    $this->registeredTickingChunks[$chunkHash],
 
 1247                    $this->recheckTickingChunks[$chunkHash],
 
 1248                    $this->validTickingChunks[$chunkHash]
 
 1251                unset($this->registeredTickingChunks[$chunkHash][$tickerId]);
 
 
 1256    private function tickChunks() : void{
 
 1257        if($this->chunkTickRadius <= 0 || count($this->registeredTickingChunks) === 0){
 
 1261        if(count($this->recheckTickingChunks) > 0){
 
 1262            $this->timings->randomChunkUpdatesChunkSelection->startTiming();
 
 1264            $chunkTickableCache = [];
 
 1266            foreach($this->recheckTickingChunks as $hash => $_){
 
 1267                World::getXZ($hash, $chunkX, $chunkZ);
 
 1268                if($this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){
 
 1269                    $this->validTickingChunks[$hash] = $hash;
 
 1272            $this->recheckTickingChunks = [];
 
 1274            $this->timings->randomChunkUpdatesChunkSelection->stopTiming();
 
 1277        foreach($this->validTickingChunks as $index => $_){
 
 1278            World::getXZ($index, $chunkX, $chunkZ);
 
 1280            $this->tickChunk($chunkX, $chunkZ);
 
 1290    private function isChunkTickable(
int $chunkX, 
int $chunkZ, array &$cache) : bool{
 
 1291        for($cx = -1; $cx <= 1; ++$cx){
 
 1292            for($cz = -1; $cz <= 1; ++$cz){
 
 1293                $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
 
 1294                if(isset($cache[$chunkHash])){
 
 1295                    if(!$cache[$chunkHash]){
 
 1300                if($this->isChunkLocked($chunkX + $cx, $chunkZ + $cz)){
 
 1301                    $cache[$chunkHash] = 
false;
 
 1304                $adjacentChunk = $this->getChunk($chunkX + $cx, $chunkZ + $cz);
 
 1305                if($adjacentChunk === 
null || !$adjacentChunk->isPopulated()){
 
 1306                    $cache[$chunkHash] = 
false;
 
 1309                $lightPopulatedState = $adjacentChunk->isLightPopulated();
 
 1310                if($lightPopulatedState !== 
true){
 
 1311                    if($lightPopulatedState === 
false){
 
 1312                        $this->orderLightPopulation($chunkX + $cx, $chunkZ + $cz);
 
 1314                    $cache[$chunkHash] = 
false;
 
 1318                $cache[$chunkHash] = 
true;
 
 1334    private function markTickingChunkForRecheck(
int $chunkX, 
int $chunkZ) : void{
 
 1335        for($cx = -1; $cx <= 1; ++$cx){
 
 1336            for($cz = -1; $cz <= 1; ++$cz){
 
 1337                $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
 
 1338                unset($this->validTickingChunks[$chunkHash]);
 
 1339                if(isset($this->registeredTickingChunks[$chunkHash])){
 
 1340                    $this->recheckTickingChunks[$chunkHash] = $chunkHash;
 
 1342                    unset($this->recheckTickingChunks[$chunkHash]);
 
 1348    private function orderLightPopulation(
int $chunkX, 
int $chunkZ) : void{
 
 1349        $chunkHash = World::chunkHash($chunkX, $chunkZ);
 
 1350        $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated();
 
 1351        if($lightPopulatedState === 
false){
 
 1352            $this->chunks[$chunkHash]->setLightPopulated(
null);
 
 1353            $this->markTickingChunkForRecheck($chunkX, $chunkZ);
 
 1355            $this->workerPool->submitTask(
new LightPopulationTask(
 
 1356                $this->chunks[$chunkHash],
 
 1357                function(array $blockLight, array $skyLight, array $heightMap) use ($chunkX, $chunkZ) : 
void{
 
 1364                    if($this->unloaded || ($chunk = $this->getChunk($chunkX, $chunkZ)) === 
null || $chunk->isLightPopulated() === 
true){
 
 1369                    $chunk->setHeightMapArray($heightMap);
 
 1370                    foreach($blockLight as $y => $lightArray){
 
 1371                        $chunk->getSubChunk($y)->setBlockLightArray($lightArray);
 
 1373                    foreach($skyLight as $y => $lightArray){
 
 1374                        $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray);
 
 1376                    $chunk->setLightPopulated(
true);
 
 1377                    $this->markTickingChunkForRecheck($chunkX, $chunkZ);
 
 1383    private function tickChunk(
int $chunkX, 
int $chunkZ) : void{
 
 1384        $chunk = $this->getChunk($chunkX, $chunkZ);
 
 1385        if($chunk === 
null){
 
 1389        foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){
 
 1390            $entity->onRandomUpdate();
 
 1393        $blockFactory = $this->blockStateRegistry;
 
 1394        foreach($chunk->getSubChunks() as $Y => $subChunk){
 
 1395            if(!$subChunk->isEmptyFast()){
 
 1397                for($i = 0; $i < $this->tickedBlocksPerSubchunkPerTick; ++$i){
 
 1400                        $k = mt_rand(0, (1 << 60) - 1);
 
 1402                    $x = $k & SubChunk::COORD_MASK;
 
 1403                    $y = ($k >> SubChunk::COORD_BIT_SIZE) & SubChunk::COORD_MASK;
 
 1404                    $z = ($k >> (SubChunk::COORD_BIT_SIZE * 2)) & SubChunk::COORD_MASK;
 
 1405                    $k >>= (SubChunk::COORD_BIT_SIZE * 3);
 
 1407                    $state = $subChunk->getBlockStateId($x, $y, $z);
 
 1409                    if(isset($this->randomTickBlocks[$state])){
 
 1410                        $block = $blockFactory->fromStateId($state);
 
 1411                        $block->position($this, $chunkX * Chunk::EDGE_LENGTH + $x, ($Y << SubChunk::COORD_BIT_SIZE) + $y, $chunkZ * Chunk::EDGE_LENGTH + $z);
 
 1412                        $block->onRandomTick();
 
 1426    public function save(
bool $force = 
false) : bool{
 
 1428        if(!$this->getAutoSave() && !$force){
 
 1432        (
new WorldSaveEvent($this))->call();
 
 1434        $timings = $this->timings->syncDataSave;
 
 1435        $timings->startTiming();
 
 1437        $this->provider->getWorldData()->setTime($this->time);
 
 1438        $this->saveChunks();
 
 1439        $this->provider->getWorldData()->save();
 
 1441        $timings->stopTiming();
 
 1446    public function saveChunks() : void{
 
 1447        $this->timings->syncChunkSave->startTiming();
 
 1449            foreach($this->chunks as $chunkHash => $chunk){
 
 1450                self::getXZ($chunkHash, $chunkX, $chunkZ);
 
 1451                $this->provider->saveChunk($chunkX, $chunkZ, 
new ChunkData(
 
 1452                    $chunk->getSubChunks(),
 
 1453                    $chunk->isPopulated(),
 
 1454                    array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk()))),
 
 1455                    array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())),
 
 1456                ), $chunk->getTerrainDirtyFlags());
 
 1457                $chunk->clearTerrainDirtyFlags();
 
 1460            $this->timings->syncChunkSave->stopTiming();
 
 1470            !$this->isInWorld($pos->x, $pos->y, $pos->z) ||
 
 1471            (isset($this->scheduledBlockUpdateQueueIndex[$index = 
World::blockHash($pos->x, $pos->y, $pos->z)]) && $this->scheduledBlockUpdateQueueIndex[$index] <= $delay)
 
 1475        $this->scheduledBlockUpdateQueueIndex[$index] = $delay;
 
 1476        $this->scheduledBlockUpdateQueue->insert(
new Vector3((
int) $pos->x, (
int) $pos->y, (
int) $pos->z), $delay + $this->server->getTick());
 
 
 1479    private function tryAddToNeighbourUpdateQueue(
int $x, 
int $y, 
int $z) : void{
 
 1480        if($this->isInWorld($x, $y, $z)){
 
 1481            $hash = World::blockHash($x, $y, $z);
 
 1482            if(!isset($this->neighbourBlockUpdateQueueIndex[$hash])){
 
 1483                $this->neighbourBlockUpdateQueue->enqueue($hash);
 
 1484                $this->neighbourBlockUpdateQueueIndex[$hash] = 
true;
 
 1495    private function internalNotifyNeighbourBlockUpdate(
int $x, 
int $y, 
int $z) : void{
 
 1496        $this->tryAddToNeighbourUpdateQueue($x, $y, $z);
 
 1497        foreach(Facing::OFFSET as [$dx, $dy, $dz]){
 
 1498            $this->tryAddToNeighbourUpdateQueue($x + $dx, $y + $dy, $z + $dz);
 
 1510        $this->internalNotifyNeighbourBlockUpdate($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ());
 
 
 1518        $minX = (int) floor($bb->minX - 1);
 
 1519        $minY = (int) floor($bb->minY - 1);
 
 1520        $minZ = (int) floor($bb->minZ - 1);
 
 1521        $maxX = (int) floor($bb->maxX + 1);
 
 1522        $maxY = (int) floor($bb->maxY + 1);
 
 1523        $maxZ = (int) floor($bb->maxZ + 1);
 
 1527        $collisionInfo = $this->blockStateRegistry->collisionInfo;
 
 1529            for($z = $minZ; $z <= $maxZ; ++$z){
 
 1530                $zOverflow = $z === $minZ || $z === $maxZ;
 
 1531                for($x = $minX; $x <= $maxX; ++$x){
 
 1532                    $zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
 
 1533                    for($y = $minY; $y <= $maxY; ++$y){
 
 1534                        $overflow = $zxOverflow || $y === $minY || $y === $maxY;
 
 1536                        $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
 
 1538                            $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
 
 1539                            match ($stateCollisionInfo) {
 
 1540                                RuntimeBlockStateRegistry::COLLISION_CUBE => 
true,
 
 1541                                RuntimeBlockStateRegistry::COLLISION_NONE => 
false,
 
 1542                                default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
 
 1545                            return [$this->getBlockAt($x, $y, $z)];
 
 1552            for($z = $minZ; $z <= $maxZ; ++$z){
 
 1553                $zOverflow = $z === $minZ || $z === $maxZ;
 
 1554                for($x = $minX; $x <= $maxX; ++$x){
 
 1555                    $zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
 
 1556                    for($y = $minY; $y <= $maxY; ++$y){
 
 1557                        $overflow = $zxOverflow || $y === $minY || $y === $maxY;
 
 1559                        $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
 
 1561                            $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
 
 1562                            match ($stateCollisionInfo) {
 
 1563                                RuntimeBlockStateRegistry::COLLISION_CUBE => 
true,
 
 1564                                RuntimeBlockStateRegistry::COLLISION_NONE => 
false,
 
 1565                                default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
 
 1568                            $collides[] = $this->getBlockAt($x, $y, $z);
 
 
 1582    private function getBlockCollisionInfo(
int $x, 
int $y, 
int $z, array $collisionInfo) : int{
 
 1583        if(!$this->isInWorld($x, $y, $z)){
 
 1584            return RuntimeBlockStateRegistry::COLLISION_NONE;
 
 1586        $chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
 
 1587        if($chunk === 
null){
 
 1588            return RuntimeBlockStateRegistry::COLLISION_NONE;
 
 1591            ->getSubChunk($y >> SubChunk::COORD_BIT_SIZE)
 
 1593                $x & SubChunk::COORD_MASK,
 
 1594                $y & SubChunk::COORD_MASK,
 
 1595                $z & SubChunk::COORD_MASK
 
 1597        return $collisionInfo[$stateId];
 
 1611    private function getBlockCollisionBoxesForCell(
int $x, 
int $y, 
int $z, array $collisionInfo) : array{
 
 1612        $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
 
 1613        $boxes = match($stateCollisionInfo){
 
 1614            RuntimeBlockStateRegistry::COLLISION_NONE => [],
 
 1615            RuntimeBlockStateRegistry::COLLISION_CUBE => [AxisAlignedBB::one()->offset($x, $y, $z)],
 
 1616            default => $this->getBlockAt($x, $y, $z)->getCollisionBoxes()
 
 1620        if($stateCollisionInfo !== RuntimeBlockStateRegistry::COLLISION_CUBE){
 
 1622            foreach(Facing::OFFSET as [$dx, $dy, $dz]){
 
 1623                $offsetX = $x + $dx;
 
 1624                $offsetY = $y + $dy;
 
 1625                $offsetZ = $z + $dz;
 
 1626                $stateCollisionInfo = $this->getBlockCollisionInfo($offsetX, $offsetY, $offsetZ, $collisionInfo);
 
 1627                if($stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW){
 
 1629                    $cellBB ??= AxisAlignedBB::one()->offset($x, $y, $z);
 
 1630                    $extraBoxes = $this->getBlockAt($offsetX, $offsetY, $offsetZ)->getCollisionBoxes();
 
 1631                    foreach($extraBoxes as $extraBox){
 
 1632                        if($extraBox->intersectsWith($cellBB)){
 
 1633                            $boxes[] = $extraBox;
 
 1648        $minX = (int) floor($bb->minX);
 
 1649        $minY = (int) floor($bb->minY);
 
 1650        $minZ = (int) floor($bb->minZ);
 
 1651        $maxX = (int) floor($bb->maxX);
 
 1652        $maxY = (int) floor($bb->maxY);
 
 1653        $maxZ = (int) floor($bb->maxZ);
 
 1657        $collisionInfo = $this->blockStateRegistry->collisionInfo;
 
 1659        for($z = $minZ; $z <= $maxZ; ++$z){
 
 1660            for($x = $minX; $x <= $maxX; ++$x){
 
 1661                $chunkPosHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
 
 1662                for($y = $minY; $y <= $maxY; ++$y){
 
 1663                    $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
 
 1665                    $boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z, $collisionInfo);
 
 1667                    foreach($boxes as $blockBB){
 
 1668                        if($blockBB->intersectsWith($bb)){
 
 1669                            $collides[] = $blockBB;
 
 
 1687        $collides = $this->getBlockCollisionBoxes($bb);
 
 1690            foreach($this->getCollidingEntities($bb->
expandedCopy(0.25, 0.25, 0.25), $entity) as $ent){
 
 1691                $collides[] = clone $ent->boundingBox;
 
 
 1703        $timeProgress = ($this->time % self::TIME_FULL) / self::TIME_FULL;
 
 1706        $sunProgress = $timeProgress + ($timeProgress < 0.25 ? 0.75 : -0.25);
 
 1710        $diff = (((1 - ((cos($sunProgress * M_PI) + 1) / 2)) - $sunProgress) / 3);
 
 1712        return $sunProgress + $diff;
 
 
 1719        return $this->sunAnglePercentage;
 
 
 1726        return $this->sunAnglePercentage * 2 * M_PI;
 
 
 1733        return $this->sunAnglePercentage * 360.0;
 
 
 1741        $percentage = max(0, min(1, -(cos($this->getSunAngleRadians()) * 2 - 0.5)));
 
 1745        return (
int) ($percentage * 11);
 
 
 1752        return $this->skyLightReduction;
 
 
 1760        $floorX = $pos->getFloorX();
 
 1761        $floorY = $pos->getFloorY();
 
 1762        $floorZ = $pos->getFloorZ();
 
 1763        return $this->getFullLightAt($floorX, $floorY, $floorZ);
 
 
 1771        $skyLight = $this->getRealBlockSkyLightAt($x, $y, $z);
 
 1773            return max($skyLight, $this->getBlockLightAt($x, $y, $z));
 
 
 1784        return $this->getHighestAdjacentLight($x, $y, $z, $this->getFullLightAt(...));
 
 
 1792        $floorX = $pos->getFloorX();
 
 1793        $floorY = $pos->getFloorY();
 
 1794        $floorZ = $pos->getFloorZ();
 
 1795        return $this->getPotentialLightAt($floorX, $floorY, $floorZ);
 
 
 1803        return max($this->getPotentialBlockSkyLightAt($x, $y, $z), $this->getBlockLightAt($x, $y, $z));
 
 
 1811        return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialLightAt(...));
 
 
 1821        if(!$this->isInWorld($x, $y, $z)){
 
 1822            return $y >= self::Y_MAX ? 15 : 0;
 
 1824        if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== 
null && $chunk->isLightPopulated() === 
true){
 
 1825            return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockSkyLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
 
 
 1836        $light = $this->getPotentialBlockSkyLightAt($x, $y, $z) - $this->skyLightReduction;
 
 1837        return $light < 0 ? 0 : $light;
 
 
 1846        if(!$this->isInWorld($x, $y, $z)){
 
 1849        if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== 
null && $chunk->isLightPopulated() === 
true){
 
 1850            return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
 
 
 1855    public function updateAllLight(
int $x, 
int $y, 
int $z) : void{
 
 1856        if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) === null || $chunk->isLightPopulated() !== true){
 
 1860        $blockFactory = $this->blockStateRegistry;
 
 1861        $this->timings->doBlockSkyLightUpdates->startTiming();
 
 1862        if($this->skyLightUpdate === 
null){
 
 1863            $this->skyLightUpdate = 
new SkyLightUpdate(
new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight);
 
 1865        $this->skyLightUpdate->recalculateNode($x, $y, $z);
 
 1866        $this->timings->doBlockSkyLightUpdates->stopTiming();
 
 1868        $this->timings->doBlockLightUpdates->startTiming();
 
 1869        if($this->blockLightUpdate === 
null){
 
 1870            $this->blockLightUpdate = 
new BlockLightUpdate(
new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->light);
 
 1872        $this->blockLightUpdate->recalculateNode($x, $y, $z);
 
 1873        $this->timings->doBlockLightUpdates->stopTiming();
 
 1879    private function getHighestAdjacentLight(
int $x, 
int $y, 
int $z, \Closure $lightGetter) : int{
 
 1881        foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
 
 1882            $x1 = $x + $offsetX;
 
 1883            $y1 = $y + $offsetY;
 
 1884            $z1 = $z + $offsetZ;
 
 1886                !$this->isInWorld($x1, $y1, $z1) ||
 
 1887                ($chunk = $this->getChunk($x1 >> Chunk::COORD_BIT_SIZE, $z1 >> Chunk::COORD_BIT_SIZE)) === 
null ||
 
 1888                $chunk->isLightPopulated() !== 
true 
 1892            $max = max($max, $lightGetter($x1, $y1, $z1));
 
 1901        return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialBlockSkyLightAt(...));
 
 
 1909        return $this->getHighestAdjacentPotentialBlockSkyLight($x, $y, $z) - $this->skyLightReduction;
 
 
 1916        return $this->getHighestAdjacentLight($x, $y, $z, $this->getBlockLightAt(...));
 
 
 1919    private function executeQueuedLightUpdates() : void{
 
 1920        if($this->blockLightUpdate !== null){
 
 1921            $this->timings->doBlockLightUpdates->startTiming();
 
 1922            $this->blockLightUpdate->execute();
 
 1923            $this->blockLightUpdate = 
null;
 
 1924            $this->timings->doBlockLightUpdates->stopTiming();
 
 1927        if($this->skyLightUpdate !== 
null){
 
 1928            $this->timings->doBlockSkyLightUpdates->startTiming();
 
 1929            $this->skyLightUpdate->execute();
 
 1930            $this->skyLightUpdate = 
null;
 
 1931            $this->timings->doBlockSkyLightUpdates->stopTiming();
 
 1938            $y < $this->maxY && $y >= $this->minY &&
 
 
 1954        return $this->getBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $cached, $addToCache);
 
 
 1966    public function getBlockAt(
int $x, 
int $y, 
int $z, 
bool $cached = 
true, 
bool $addToCache = 
true) : 
Block{
 
 1967        $relativeBlockHash = null;
 
 1968        $chunkHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
 
 1970        if($this->isInWorld($x, $y, $z)){
 
 1971            $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
 
 1973            if($cached && isset($this->blockCache[$chunkHash][$relativeBlockHash])){
 
 1974                return $this->blockCache[$chunkHash][$relativeBlockHash];
 
 1977            $chunk = $this->chunks[$chunkHash] ?? 
null;
 
 1978            if($chunk !== 
null){
 
 1979                $block = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK));
 
 1981                $addToCache = 
false;
 
 1982                $block = VanillaBlocks::AIR();
 
 1985            $block = VanillaBlocks::AIR();
 
 1988        $block->position($this, $x, $y, $z);
 
 1990        if($this->inDynamicStateRecalculation){
 
 1994            $addToCache = 
false;
 
 1996            $this->inDynamicStateRecalculation = 
true;
 
 1997            $replacement = $block->readStateFromWorld();
 
 1998            if($replacement !== $block){
 
 1999                $replacement->position($this, $x, $y, $z);
 
 2000                $block = $replacement;
 
 2002            $this->inDynamicStateRecalculation = 
false;
 
 2005        if($addToCache && $relativeBlockHash !== 
null){
 
 2006            $this->blockCache[$chunkHash][$relativeBlockHash] = $block;
 
 2008            if(++$this->blockCacheSize >= self::BLOCK_CACHE_SIZE_CAP){
 
 2009                $this->trimBlockCache();
 
 
 2022        $this->setBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $block, $update);
 
 
 2033    public function setBlockAt(
int $x, 
int $y, 
int $z, 
Block $block, 
bool $update = 
true) : void{
 
 2034        if(!$this->isInWorld($x, $y, $z)){
 
 2035            throw new \InvalidArgumentException(
"Pos x=$x,y=$y,z=$z is outside of the world bounds");
 
 2037        $chunkX = $x >> Chunk::COORD_BIT_SIZE;
 
 2038        $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
 
 2039        if($this->loadChunk($chunkX, $chunkZ) === 
null){ 
 
 2040            throw new WorldException(
"Cannot set a block in un-generated terrain");
 
 2044        $stateId = $block->getStateId();
 
 2045        if(!$this->blockStateRegistry->hasStateId($stateId)){
 
 2046            throw new \LogicException(
"Block state ID not known to RuntimeBlockStateRegistry (probably not registered)");
 
 2048        if(!GlobalBlockStateHandlers::getSerializer()->isRegistered($block)){
 
 2049            throw new \LogicException(
"Block not registered with GlobalBlockStateHandlers serializer");
 
 2052        $this->timings->setBlock->startTiming();
 
 2054        $this->unlockChunk($chunkX, $chunkZ, 
null);
 
 2056        $block = clone $block;
 
 2058        $block->position($this, $x, $y, $z);
 
 2059        $block->writeStateToWorld();
 
 2060        $pos = 
new Vector3($x, $y, $z);
 
 2062        $chunkHash = World::chunkHash($chunkX, $chunkZ);
 
 2063        $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
 
 2065        unset($this->blockCache[$chunkHash][$relativeBlockHash]);
 
 2066        $this->blockCacheSize--;
 
 2067        unset($this->blockCollisionBoxCache[$chunkHash][$relativeBlockHash]);
 
 2070        foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
 
 2071            $sideChunkPosHash = World::chunkHash(($x + $offsetX) >> Chunk::COORD_BIT_SIZE, ($z + $offsetZ) >> Chunk::COORD_BIT_SIZE);
 
 2072            $sideChunkBlockHash = World::chunkBlockHash($x + $offsetX, $y + $offsetY, $z + $offsetZ);
 
 2073            unset($this->blockCollisionBoxCache[$sideChunkPosHash][$sideChunkBlockHash]);
 
 2076        if(!isset($this->changedBlocks[$chunkHash])){
 
 2077            $this->changedBlocks[$chunkHash] = [];
 
 2079        $this->changedBlocks[$chunkHash][$relativeBlockHash] = $pos;
 
 2081        foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
 
 2082            $listener->onBlockChanged($pos);
 
 2086            $this->updateAllLight($x, $y, $z);
 
 2087            $this->internalNotifyNeighbourBlockUpdate($x, $y, $z);
 
 2090        $this->timings->setBlock->stopTiming();
 
 
 2093    public function dropItem(Vector3 $source, Item $item, ?Vector3 $motion = 
null, 
int $delay = 10) : ?ItemEntity{
 
 2094        if($item->isNull()){
 
 2098        $itemEntity = 
new ItemEntity(Location::fromObject($source, $this, Utils::getRandomFloat() * 360, 0), $item);
 
 2100        $itemEntity->setPickupDelay($delay);
 
 2101        $itemEntity->setMotion($motion ?? 
new Vector3(Utils::getRandomFloat() * 0.2 - 0.1, 0.2, Utils::getRandomFloat() * 0.2 - 0.1));
 
 2102        $itemEntity->spawnToAll();
 
 2116        foreach(ExperienceOrb::splitIntoOrbSizes($amount) as $split){
 
 2117            $orb = 
new ExperienceOrb(Location::fromObject($pos, $this, Utils::getRandomFloat() * 360, 0), $split);
 
 2119            $orb->setMotion(
new Vector3((Utils::getRandomFloat() * 0.2 - 0.1) * 2, Utils::getRandomFloat() * 0.4, (Utils::getRandomFloat() * 0.2 - 0.1) * 2));
 
 
 2136    public function useBreakOn(
Vector3 $vector, ?
Item &$item = 
null, ?
Player $player = 
null, 
bool $createParticles = 
false, array &$returnedItems = []) : bool{
 
 2137        $vector = $vector->floor();
 
 2139        $chunkX = $vector->getFloorX() >> Chunk::COORD_BIT_SIZE;
 
 2140        $chunkZ = $vector->getFloorZ() >> Chunk::COORD_BIT_SIZE;
 
 2141        if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
 
 2145        $target = $this->getBlock($vector);
 
 2146        $affectedBlocks = $target->getAffectedBlocks();
 
 2149            $item = VanillaItems::AIR();
 
 2153        if($player === 
null || $player->hasFiniteResources()){
 
 2154            $drops = array_merge(...array_map(fn(Block $block) => $block->getDrops($item), $affectedBlocks));
 
 2158        if($player !== 
null && $player->hasFiniteResources()){
 
 2159            $xpDrop = array_sum(array_map(fn(Block $block) => $block->getXpDropForTool($item), $affectedBlocks));
 
 2162        if($player !== 
null){
 
 2163            $ev = 
new BlockBreakEvent($player, $target, $item, $player->isCreative(), $drops, $xpDrop);
 
 2165            if($target instanceof Air || ($player->isSurvival() && !$target->getBreakInfo()->isBreakable()) || $player->isSpectator()){
 
 2169            if($player->isAdventure(
true) && !$ev->isCancelled()){
 
 2171                $itemParser = LegacyStringToItemParser::getInstance();
 
 2172                foreach($item->getCanDestroy() as $v){
 
 2173                    $entry = $itemParser->parse($v);
 
 2174                    if($entry->getBlock()->hasSameTypeId($target)){
 
 2186            if($ev->isCancelled()){
 
 2190            $drops = $ev->getDrops();
 
 2191            $xpDrop = $ev->getXpDropAmount();
 
 2193        }elseif(!$target->getBreakInfo()->isBreakable()){
 
 2197        foreach($affectedBlocks as $t){
 
 2198            $this->destroyBlockInternal($t, $item, $player, $createParticles, $returnedItems);
 
 2201        $item->onDestroyBlock($target, $returnedItems);
 
 2203        if(count($drops) > 0){
 
 2204            $dropPos = $vector->add(0.5, 0.5, 0.5);
 
 2205            foreach($drops as $drop){
 
 2206                if(!$drop->isNull()){
 
 2207                    $this->dropItem($dropPos, $drop);
 
 2213            $this->dropExperience($vector->add(0.5, 0.5, 0.5), $xpDrop);
 
 
 2222    private function destroyBlockInternal(Block $target, Item $item, ?Player $player, 
bool $createParticles, array &$returnedItems) : void{
 
 2223        if($createParticles){
 
 2224            $this->addParticle($target->getPosition()->add(0.5, 0.5, 0.5), 
new BlockBreakParticle($target));
 
 2227        $target->onBreak($item, $player, $returnedItems);
 
 2229        $tile = $this->getTile($target->getPosition());
 
 2231            $tile->onBlockDestroyed();
 
 2242    public function useItemOn(
Vector3 $vector, 
Item &$item, 
int $face, ?
Vector3 $clickVector = 
null, ?
Player $player = 
null, 
bool $playSound = 
false, array &$returnedItems = []) : bool{
 
 2243        $blockClicked = $this->getBlock($vector);
 
 2244        $blockReplace = $blockClicked->getSide($face);
 
 2246        if($clickVector === 
null){
 
 2247            $clickVector = 
new Vector3(0.0, 0.0, 0.0);
 
 2250                min(1.0, max(0.0, $clickVector->x)),
 
 2251                min(1.0, max(0.0, $clickVector->y)),
 
 2252                min(1.0, max(0.0, $clickVector->z))
 
 2256        if(!$this->isInWorld($blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z)){
 
 2260        $chunkX = $blockReplace->getPosition()->getFloorX() >> Chunk::COORD_BIT_SIZE;
 
 2261        $chunkZ = $blockReplace->getPosition()->getFloorZ() >> Chunk::COORD_BIT_SIZE;
 
 2262        if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
 
 2266        if($blockClicked->getTypeId() === BlockTypeIds::AIR){
 
 2270        if($player !== 
null){
 
 2271            $ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK);
 
 2272            if($player->isSneaking()){
 
 2273                $ev->setUseItem(false);
 
 2274                $ev->setUseBlock($item->isNull()); 
 
 2276            if($player->isSpectator()){
 
 2281            if(!$ev->isCancelled()){
 
 2282                if($ev->useBlock() && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
 
 2287                    $result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems);
 
 2288                    if($result !== ItemUseResult::NONE){
 
 2289                        return $result === ItemUseResult::SUCCESS;
 
 2295        }elseif($blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
 
 2299        if($item->isNull() || !$item->canBePlaced()){
 
 2307            $item->getPlacementTransaction($blockClicked, $blockClicked, Facing::UP, $clickVector, $player) ??
 
 2308            $item->getPlacementTransaction($blockReplace, $blockClicked, $face, $clickVector, $player);
 
 2314        foreach($tx->getBlocks() as [$x, $y, $z, $block]){
 
 2315            $block->position($this, $x, $y, $z);
 
 2316            foreach($block->getCollisionBoxes() as $collisionBox){
 
 2317                if(count($this->getCollidingEntities($collisionBox)) > 0){
 
 2323        if($player !== 
null){
 
 2324            $ev = 
new BlockPlaceEvent($player, $tx, $blockClicked, $item);
 
 2325            if($player->isSpectator()){
 
 2329            if($player->isAdventure(
true) && !$ev->isCancelled()){
 
 2331                $itemParser = LegacyStringToItemParser::getInstance();
 
 2333                    $entry = $itemParser->parse($v);
 
 2334                    if($entry->getBlock()->hasSameTypeId($blockClicked)){
 
 2346            if($ev->isCancelled()){
 
 2355        foreach($tx->getBlocks() as [$x, $y, $z, $_]){
 
 2356            $tile = $this->getTileAt($x, $y, $z);
 
 2359                $tile->copyDataFromItem($item);
 
 2362            $placed = $this->getBlockAt($x, $y, $z);
 
 2363            $placed->onPostPlace();
 
 2364            if($first && $playSound){
 
 2365                $this->addSound($placed->getPosition(), 
new BlockPlaceSound($placed));
 
 
 2375    public function getEntity(
int $entityId) : ?Entity{
 
 2376        return $this->entities[$entityId] ?? null;
 
 2386        return $this->entities;
 
 
 2402        foreach($this->getNearbyEntities($bb, $entity) as $ent){
 
 2403            if($ent->canBeCollidedWith() && ($entity === 
null || $entity->canCollideWith($ent))){
 
 
 2420        $minX = ((int) floor($bb->minX - 2)) >> Chunk::COORD_BIT_SIZE;
 
 2421        $maxX = ((int) floor($bb->maxX + 2)) >> Chunk::COORD_BIT_SIZE;
 
 2422        $minZ = ((int) floor($bb->minZ - 2)) >> Chunk::COORD_BIT_SIZE;
 
 2423        $maxZ = ((int) floor($bb->maxZ + 2)) >> Chunk::COORD_BIT_SIZE;
 
 2425        for($x = $minX; $x <= $maxX; ++$x){
 
 2426            for($z = $minZ; $z <= $maxZ; ++$z){
 
 2427                foreach($this->getChunkEntities($x, $z) as $ent){
 
 2428                    if($ent !== $entity && $ent->boundingBox->intersectsWith($bb)){
 
 
 2450        assert(is_a($entityType, 
Entity::class, true));
 
 2452        $minX = ((int) floor($pos->x - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
 
 2453        $maxX = ((int) floor($pos->x + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
 
 2454        $minZ = ((int) floor($pos->z - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
 
 2455        $maxZ = ((int) floor($pos->z + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
 
 2457        $currentTargetDistSq = $maxDistance ** 2;
 
 2463        $currentTarget = 
null;
 
 2465        for($x = $minX; $x <= $maxX; ++$x){
 
 2466            for($z = $minZ; $z <= $maxZ; ++$z){
 
 2467                foreach($this->getChunkEntities($x, $z) as $entity){
 
 2468                    if(!($entity instanceof $entityType) || $entity->isFlaggedForDespawn() || (!$includeDead && !$entity->isAlive())){
 
 2471                    $distSq = $entity->getPosition()->distanceSquared($pos);
 
 2472                    if($distSq < $currentTargetDistSq){
 
 2473                        $currentTargetDistSq = $distSq;
 
 2474                        $currentTarget = $entity;
 
 2480        return $currentTarget;
 
 
 2490        return $this->players;
 
 
 2500        return $this->getTileAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z));
 
 
 2507        return ($chunk = $this->loadChunk($x >> 
Chunk::COORD_BIT_SIZE, $z >> 
Chunk::COORD_BIT_SIZE)) !== null ? $chunk->getTile($x & 
Chunk::COORD_MASK, $y, $z & 
Chunk::COORD_MASK) : null;
 
 
 2510    public function getBiomeId(
int $x, 
int $y, 
int $z) : int{
 
 2511        if(($chunk = $this->loadChunk($x >> 
Chunk::COORD_BIT_SIZE, $z >> 
Chunk::COORD_BIT_SIZE)) !== null){
 
 2512            return $chunk->getBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
 
 2514        return BiomeIds::OCEAN; 
 
 2517    public function getBiome(
int $x, 
int $y, 
int $z) : Biome{
 
 2518        return BiomeRegistry::getInstance()->getBiome($this->getBiomeId($x, $y, $z));
 
 2521    public function setBiomeId(
int $x, 
int $y, 
int $z, 
int $biomeId) : void{
 
 2522        $chunkX = $x >> Chunk::COORD_BIT_SIZE;
 
 2523        $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
 
 2524        $this->unlockChunk($chunkX, $chunkZ, 
null);
 
 2525        if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== 
null){
 
 2526            $chunk->setBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId);
 
 2529            throw new WorldException(
"Cannot set biome in a non-generated chunk");
 
 2538        return $this->chunks;
 
 
 2541    public function getChunk(
int $chunkX, 
int $chunkZ) : ?
Chunk{
 
 2542        return $this->chunks[
World::chunkHash($chunkX, $chunkZ)] ?? null;
 
 2550        return $this->entitiesByChunk[
World::chunkHash($chunkX, $chunkZ)] ?? [];
 
 
 2557        return $this->loadChunk($pos->getFloorX() >> 
Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> 
Chunk::COORD_BIT_SIZE);
 
 
 2568        for($xx = -1; $xx <= 1; ++$xx){
 
 2569            for($zz = -1; $zz <= 1; ++$zz){
 
 2570                if($xx === 0 && $zz === 0){
 
 2573                $result[World::chunkHash($xx, $zz)] = $this->loadChunk($x + $xx, $z + $zz);
 
 
 2595        $chunkHash = 
World::chunkHash($chunkX, $chunkZ);
 
 2596        if(isset($this->chunkLock[$chunkHash])){
 
 2597            throw new \InvalidArgumentException(
"Chunk $chunkX $chunkZ is already locked");
 
 2599        $this->chunkLock[$chunkHash] = $lockId;
 
 2600        $this->markTickingChunkForRecheck($chunkX, $chunkZ);
 
 
 2612        $chunkHash = 
World::chunkHash($chunkX, $chunkZ);
 
 2613        if(isset($this->chunkLock[$chunkHash]) && ($lockId === 
null || $this->chunkLock[$chunkHash] === $lockId)){
 
 2614            unset($this->chunkLock[$chunkHash]);
 
 2615            $this->markTickingChunkForRecheck($chunkX, $chunkZ);
 
 
 2627        return isset($this->chunkLock[
World::chunkHash($chunkX, $chunkZ)]);
 
 
 2630    public function setChunk(
int $chunkX, 
int $chunkZ, 
Chunk $chunk) : void{
 
 2631        foreach($chunk->getSubChunks() as $subChunk){
 
 2632            foreach($subChunk->getBlockLayers() as $blockLayer){
 
 2633                foreach($blockLayer->getPalette() as $blockStateId){
 
 2634                    if(!$this->blockStateRegistry->hasStateId($blockStateId)){
 
 2635                        throw new \InvalidArgumentException(
"Provided chunk contains unknown/unregistered blocks (found unknown state ID $blockStateId)");
 
 2641        $chunkHash = World::chunkHash($chunkX, $chunkZ);
 
 2642        $oldChunk = $this->loadChunk($chunkX, $chunkZ);
 
 2643        if($oldChunk !== 
null && $oldChunk !== $chunk){
 
 2645            $transferredTiles = 0;
 
 2646            foreach($oldChunk->getTiles() as $oldTile){
 
 2647                $tilePosition = $oldTile->getPosition();
 
 2648                $localX = $tilePosition->getFloorX() & Chunk::COORD_MASK;
 
 2649                $localY = $tilePosition->getFloorY();
 
 2650                $localZ = $tilePosition->getFloorZ() & Chunk::COORD_MASK;
 
 2652                $newBlock = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($localX, $localY, $localZ));
 
 2653                $expectedTileClass = $newBlock->getIdInfo()->getTileClass();
 
 2655                    $expectedTileClass === 
null || 
 
 2656                    !($oldTile instanceof $expectedTileClass) || 
 
 2657                    (($newTile = $chunk->getTile($localX, $localY, $localZ)) !== 
null && $newTile !== $oldTile) 
 
 2662                    $transferredTiles++;
 
 2663                    $chunk->addTile($oldTile);
 
 2664                    $oldChunk->removeTile($oldTile);
 
 2667            if($deletedTiles > 0 || $transferredTiles > 0){
 
 2668                $this->logger->debug(
"Replacement of chunk $chunkX $chunkZ caused deletion of $deletedTiles obsolete/conflicted tiles, and transfer of $transferredTiles");
 
 2672        $this->chunks[$chunkHash] = $chunk;
 
 2673        unset($this->knownUngeneratedChunks[$chunkHash]);
 
 2675        $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
 
 2676        unset($this->blockCache[$chunkHash]);
 
 2677        unset($this->blockCollisionBoxCache[$chunkHash]);
 
 2678        unset($this->changedBlocks[$chunkHash]);
 
 2679        $chunk->setTerrainDirty();
 
 2680        $this->markTickingChunkForRecheck($chunkX, $chunkZ); 
 
 2682        if(!$this->isChunkInUse($chunkX, $chunkZ)){
 
 2683            $this->unloadChunkRequest($chunkX, $chunkZ);
 
 2686        if($oldChunk === 
null){
 
 2687            if(ChunkLoadEvent::hasHandlers()){
 
 2688                (
new ChunkLoadEvent($this, $chunkX, $chunkZ, $chunk, 
true))->call();
 
 2691            foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
 
 2692                $listener->onChunkLoaded($chunkX, $chunkZ, $chunk);
 
 2695            foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
 
 2696                $listener->onChunkChanged($chunkX, $chunkZ, $chunk);
 
 2700        for($cX = -1; $cX <= 1; ++$cX){
 
 2701            for($cZ = -1; $cZ <= 1; ++$cZ){
 
 2702                foreach($this->getChunkEntities($chunkX + $cX, $chunkZ + $cZ) as $entity){
 
 2703                    $entity->onNearbyBlockChange();
 
 2716        if(($chunk = $this->loadChunk($x >> 
Chunk::COORD_BIT_SIZE, $z >> 
Chunk::COORD_BIT_SIZE)) !== null){
 
 2717            return $chunk->getHighestBlockAt($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
 
 2719        throw new WorldException(
"Cannot get highest block in an ungenerated chunk");
 
 
 2726        return $this->isChunkLoaded($pos->getFloorX() >> 
Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> 
Chunk::COORD_BIT_SIZE);
 
 
 2729    public function isChunkLoaded(
int $x, 
int $z) : bool{
 
 2730        return isset($this->chunks[
World::chunkHash($x, $z)]);
 
 2733    public function isChunkGenerated(
int $x, 
int $z) : bool{
 
 2734        return $this->loadChunk($x, $z) !== null;
 
 2737    public function isChunkPopulated(
int $x, 
int $z) : bool{
 
 2738        $chunk = $this->loadChunk($x, $z);
 
 2739        return $chunk !== 
null && $chunk->isPopulated();
 
 2746        return 
Position::fromObject($this->provider->getWorldData()->getSpawn(), $this);
 
 
 2753        $previousSpawn = $this->getSpawnLocation();
 
 2754        $this->provider->getWorldData()->setSpawn($pos);
 
 2757        $location = Position::fromObject($pos, $this);
 
 2758        foreach($this->players as $player){
 
 2759            $player->getNetworkSession()->syncWorldSpawnPoint($location);
 
 
 2767        if($entity->isClosed()){
 
 2768            throw new \InvalidArgumentException(
"Attempted to add a garbage closed Entity to world");
 
 2770        if($entity->getWorld() !== $this){
 
 2771            throw new \InvalidArgumentException(
"Invalid Entity world");
 
 2773        if(array_key_exists($entity->getId(), $this->entities)){
 
 2774            if($this->entities[$entity->getId()] === $entity){
 
 2775                throw new \InvalidArgumentException(
"Entity " . $entity->getId() . 
" has already been added to this world");
 
 2777                throw new AssumptionFailedError(
"Found two different entities sharing entity ID " . $entity->getId());
 
 2780        if(!EntityFactory::getInstance()->isRegistered($entity::class) && !$entity instanceof NeverSavedWithChunkEntity){
 
 2783            throw new \LogicException(
"Entity " . $entity::class . 
" is not registered for a save ID in EntityFactory");
 
 2785        $pos = $entity->getPosition()->asVector3();
 
 2786        $this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity;
 
 2787        $this->entityLastKnownPositions[$entity->getId()] = $pos;
 
 2789        if($entity instanceof Player){
 
 2790            $this->players[$entity->getId()] = $entity;
 
 2792        $this->entities[$entity->getId()] = $entity;
 
 
 2801        if($entity->getWorld() !== $this){
 
 2802            throw new \InvalidArgumentException(
"Invalid Entity world");
 
 2804        if(!array_key_exists($entity->getId(), $this->entities)){
 
 2805            throw new \InvalidArgumentException(
"Entity is not tracked by this world (possibly already removed?)");
 
 2807        $pos = $this->entityLastKnownPositions[$entity->getId()];
 
 2808        $chunkHash = World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
 
 2809        if(isset($this->entitiesByChunk[$chunkHash][$entity->getId()])){
 
 2810            if(count($this->entitiesByChunk[$chunkHash]) === 1){
 
 2811                unset($this->entitiesByChunk[$chunkHash]);
 
 2813                unset($this->entitiesByChunk[$chunkHash][$entity->getId()]);
 
 2816        unset($this->entityLastKnownPositions[$entity->getId()]);
 
 2818        if($entity instanceof Player){
 
 2819            unset($this->players[$entity->getId()]);
 
 2820            $this->checkSleep();
 
 2823        unset($this->entities[$entity->getId()]);
 
 2824        unset($this->updateEntities[$entity->getId()]);
 
 
 2830    public function onEntityMoved(Entity $entity) : void{
 
 2831        if(!array_key_exists($entity->getId(), $this->entityLastKnownPositions)){
 
 2835        $oldPosition = $this->entityLastKnownPositions[$entity->getId()];
 
 2836        $newPosition = $entity->getPosition();
 
 2838        $oldChunkX = $oldPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
 
 2839        $oldChunkZ = $oldPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
 
 2840        $newChunkX = $newPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
 
 2841        $newChunkZ = $newPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
 
 2843        if($oldChunkX !== $newChunkX || $oldChunkZ !== $newChunkZ){
 
 2844            $oldChunkHash = World::chunkHash($oldChunkX, $oldChunkZ);
 
 2845            if(isset($this->entitiesByChunk[$oldChunkHash][$entity->getId()])){
 
 2846                if(count($this->entitiesByChunk[$oldChunkHash]) === 1){
 
 2847                    unset($this->entitiesByChunk[$oldChunkHash]);
 
 2849                    unset($this->entitiesByChunk[$oldChunkHash][$entity->getId()]);
 
 2853            $newViewers = $this->getViewersForPosition($newPosition);
 
 2855                if(!isset($newViewers[spl_object_id($player)])){
 
 2858                    unset($newViewers[spl_object_id($player)]);
 
 2861            foreach($newViewers as $player){
 
 2862                $entity->spawnTo($player);
 
 2865            $newChunkHash = World::chunkHash($newChunkX, $newChunkZ);
 
 2866            $this->entitiesByChunk[$newChunkHash][$entity->getId()] = $entity;
 
 2868        $this->entityLastKnownPositions[$entity->getId()] = $newPosition->asVector3();
 
 2875    public function addTile(Tile $tile) : void{
 
 2876        if($tile->isClosed()){
 
 2877            throw new \InvalidArgumentException(
"Attempted to add a garbage closed Tile to world");
 
 2879        $pos = $tile->getPosition();
 
 2880        if(!$pos->isValid() || $pos->getWorld() !== $this){
 
 2881            throw new \InvalidArgumentException(
"Invalid Tile world");
 
 2883        if(!$this->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ())){
 
 2884            throw new \InvalidArgumentException(
"Tile position is outside the world bounds");
 
 2886        if(!TileFactory::getInstance()->isRegistered($tile::class)){
 
 2887            throw new \LogicException(
"Tile " . $tile::class . 
" is not registered for a save ID in TileFactory");
 
 2890        $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
 
 2891        $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
 
 2893        if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
 
 2894            $this->chunks[$hash]->addTile($tile);
 
 2896            throw new \InvalidArgumentException(
"Attempted to create tile " . get_class($tile) . 
" in unloaded chunk $chunkX $chunkZ");
 
 2900        $this->scheduleDelayedBlockUpdate($pos->
asVector3(), 1);
 
 2907    public function removeTile(Tile $tile) : void{
 
 2909        if(!$pos->isValid() || $pos->getWorld() !== $this){
 
 2910            throw new \InvalidArgumentException(
"Invalid Tile world");
 
 2913        $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
 
 2914        $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
 
 2916        if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
 
 2917            $this->chunks[$hash]->removeTile($tile);
 
 2919        foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
 
 2920            $listener->onBlockChanged($pos->
asVector3());
 
 2924    public function isChunkInUse(
int $x, 
int $z) : bool{
 
 2925        return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) && count($this->chunkLoaders[$index]) > 0;
 
 2935        if(isset($this->chunks[$chunkHash = 
World::chunkHash($x, $z)])){
 
 2936            return $this->chunks[$chunkHash];
 
 2938        if(isset($this->knownUngeneratedChunks[$chunkHash])){
 
 2942        $this->timings->syncChunkLoad->startTiming();
 
 2944        $this->cancelUnloadChunkRequest($x, $z);
 
 2946        $this->timings->syncChunkLoadData->startTiming();
 
 2948        $loadedChunkData = 
null;
 
 2951            $loadedChunkData = $this->provider->loadChunk($x, $z);
 
 2952        }
catch(CorruptedChunkException $e){
 
 2953            $this->logger->critical(
"Failed to load chunk x=$x z=$z: " . $e->getMessage());
 
 2956        $this->timings->syncChunkLoadData->stopTiming();
 
 2958        if($loadedChunkData === 
null){
 
 2959            $this->timings->syncChunkLoad->stopTiming();
 
 2960            $this->knownUngeneratedChunks[$chunkHash] = 
true;
 
 2964        $chunkData = $loadedChunkData->getData();
 
 2965        $chunk = 
new Chunk($chunkData->getSubChunks(), $chunkData->isPopulated());
 
 2966        if(!$loadedChunkData->isUpgraded()){
 
 2967            $chunk->clearTerrainDirtyFlags();
 
 2969            $this->logger->debug(
"Chunk $x $z has been upgraded, will be saved at the next autosave opportunity");
 
 2971        $this->chunks[$chunkHash] = $chunk;
 
 2973        $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
 
 2974        unset($this->blockCache[$chunkHash]);
 
 2975        unset($this->blockCollisionBoxCache[$chunkHash]);
 
 2977        $this->initChunk($x, $z, $chunkData, $chunk);
 
 2979        if(ChunkLoadEvent::hasHandlers()){
 
 2980            (
new ChunkLoadEvent($this, $x, $z, $this->chunks[$chunkHash], 
false))->call();
 
 2983        if(!$this->isChunkInUse($x, $z)){
 
 2984            $this->logger->debug(
"Newly loaded chunk $x $z has no loaders registered, will be unloaded at next available opportunity");
 
 2985            $this->unloadChunkRequest($x, $z);
 
 2987        foreach($this->getChunkListeners($x, $z) as $listener){
 
 2988            $listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]);
 
 2990        $this->markTickingChunkForRecheck($x, $z); 
 
 2992        $this->timings->syncChunkLoad->stopTiming();
 
 2994        return $this->chunks[$chunkHash];
 
 
 2997    private function initChunk(
int $chunkX, 
int $chunkZ, ChunkData $chunkData, Chunk $chunk) : void{
 
 2998        $logger = new \
PrefixedLogger($this->logger, 
"Loading chunk $chunkX $chunkZ");
 
 3000        if(count($chunkData->getEntityNBT()) !== 0){
 
 3001            $this->timings->syncChunkLoadEntities->startTiming();
 
 3002            $entityFactory = EntityFactory::getInstance();
 
 3004            $deletedEntities = [];
 
 3005            foreach($chunkData->getEntityNBT() as $k => $nbt){
 
 3007                    $entity = $entityFactory->createFromData($this, $nbt);
 
 3008                }catch(SavedDataLoadingException $e){
 
 3009                    $logger->error(
"Bad entity data at list position $k: " . $e->getMessage());
 
 3010                    $logger->logException($e);
 
 3013                if($entity === null){
 
 3014                    $saveIdTag = $nbt->getTag(
"identifier") ?? $nbt->getTag(
"id");
 
 3015                    $saveId = 
"<unknown>";
 
 3016                    if($saveIdTag instanceof StringTag){
 
 3017                        $saveId = $saveIdTag->getValue();
 
 3018                    }elseif($saveIdTag instanceof IntTag){ 
 
 3019                        $saveId = 
"legacy(" . $saveIdTag->getValue() . 
")";
 
 3021                    $deletedEntities[$saveId] = ($deletedEntities[$saveId] ?? 0) + 1;
 
 3027            foreach(Utils::promoteKeys($deletedEntities) as $saveId => $count){
 
 3028                $logger->warning(
"Deleted unknown entity type $saveId x$count");
 
 3030            $this->timings->syncChunkLoadEntities->stopTiming();
 
 3033        if(count($chunkData->getTileNBT()) !== 0){
 
 3034            $this->timings->syncChunkLoadTileEntities->startTiming();
 
 3035            $tileFactory = TileFactory::getInstance();
 
 3038            foreach($chunkData->getTileNBT() as $k => $nbt){
 
 3040                    $tile = $tileFactory->createFromData($this, $nbt);
 
 3041                }catch(SavedDataLoadingException $e){
 
 3042                    $logger->error(
"Bad tile entity data at list position $k: " . $e->getMessage());
 
 3043                    $logger->logException($e);
 
 3047                    $saveId = $nbt->getString(
"id", 
"<unknown>");
 
 3048                    $deletedTiles[$saveId] = ($deletedTiles[$saveId] ?? 0) + 1;
 
 3052                $tilePosition = $tile->getPosition();
 
 3053                if(!$this->isChunkLoaded($tilePosition->getFloorX() >> Chunk::COORD_BIT_SIZE, $tilePosition->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
 
 3054                    $logger->error(
"Found tile saved on wrong chunk - unable to fix due to correct chunk not loaded");
 
 3055                }elseif(!$this->isInWorld($tilePosition->getFloorX(), $tilePosition->getFloorY(), $tilePosition->getFloorZ())){
 
 3056                    $logger->error(
"Cannot add tile with position outside the world bounds: x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z");
 
 3057                }elseif($this->getTile($tilePosition) !== null){
 
 3058                    $logger->error(
"Cannot add tile at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z: Another tile is already at that position");
 
 3060                    $this->addTile($tile);
 
 3062                $expectedStateId = $chunk->getBlockStateId($tilePosition->getFloorX() & Chunk::COORD_MASK, $tilePosition->getFloorY(), $tilePosition->getFloorZ() & Chunk::COORD_MASK);
 
 3063                $actualStateId = $this->getBlock($tilePosition)->getStateId();
 
 3064                if($expectedStateId !== $actualStateId){
 
 3069                    $chunk->setBlockStateId($tilePosition->getFloorX() & Chunk::COORD_MASK, $tilePosition->getFloorY(), $tilePosition->getFloorZ() & Chunk::COORD_MASK, $actualStateId);
 
 3070                    $this->logger->debug(
"Tile " . $tile::class . 
" at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z updated block state ID from $expectedStateId to $actualStateId");
 
 3074            foreach(Utils::promoteKeys($deletedTiles) as $saveId => $count){
 
 3075                $logger->warning(
"Deleted unknown tile entity type $saveId x$count");
 
 3078            $this->timings->syncChunkLoadTileEntities->stopTiming();
 
 3082    private function queueUnloadChunk(
int $x, 
int $z) : void{
 
 3083        $this->unloadQueue[World::chunkHash($x, $z)] = microtime(true);
 
 3086    public function unloadChunkRequest(
int $x, 
int $z, 
bool $safe = 
true) : bool{
 
 3087        if(($safe && $this->isChunkInUse($x, $z)) || $this->isSpawnChunk($x, $z)){
 
 3091        $this->queueUnloadChunk($x, $z);
 
 3096    public function cancelUnloadChunkRequest(
int $x, 
int $z) : void{
 
 3097        unset($this->unloadQueue[World::chunkHash($x, $z)]);
 
 3100    public function unloadChunk(
int $x, 
int $z, 
bool $safe = 
true, 
bool $trySave = 
true) : bool{
 
 3101        if($safe && $this->isChunkInUse($x, $z)){
 
 3105        if(!$this->isChunkLoaded($x, $z)){
 
 3109        $this->timings->doChunkUnload->startTiming();
 
 3111        $chunkHash = World::chunkHash($x, $z);
 
 3113        $chunk = $this->chunks[$chunkHash] ?? 
null;
 
 3115        if($chunk !== 
null){
 
 3116            if(ChunkUnloadEvent::hasHandlers()){
 
 3117                $ev = 
new ChunkUnloadEvent($this, $x, $z, $chunk);
 
 3119                if($ev->isCancelled()){
 
 3120                    $this->timings->doChunkUnload->stopTiming();
 
 3126            if($trySave && $this->getAutoSave()){
 
 3127                $this->timings->syncChunkSave->startTiming();
 
 3129                    $this->provider->saveChunk($x, $z, 
new ChunkData(
 
 3130                        $chunk->getSubChunks(),
 
 3131                        $chunk->isPopulated(),
 
 3132                        array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk()))),
 
 3133                        array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())),
 
 3134                    ), $chunk->getTerrainDirtyFlags());
 
 3136                    $this->timings->syncChunkSave->stopTiming();
 
 3140            foreach($this->getChunkListeners($x, $z) as $listener){
 
 3141                $listener->onChunkUnloaded($x, $z, $chunk);
 
 3144            foreach($this->getChunkEntities($x, $z) as $entity){
 
 3145                if($entity instanceof Player){
 
 3154        unset($this->chunks[$chunkHash]);
 
 3155        $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
 
 3156        unset($this->blockCache[$chunkHash]);
 
 3157        unset($this->blockCollisionBoxCache[$chunkHash]);
 
 3158        unset($this->changedBlocks[$chunkHash]);
 
 3159        unset($this->registeredTickingChunks[$chunkHash]);
 
 3160        $this->markTickingChunkForRecheck($x, $z);
 
 3162        if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){
 
 3163            $this->logger->debug(
"Rejecting population promise for chunk $x $z");
 
 3164            $this->chunkPopulationRequestMap[$chunkHash]->reject();
 
 3165            unset($this->chunkPopulationRequestMap[$chunkHash]);
 
 3166            if(isset($this->activeChunkPopulationTasks[$chunkHash])){
 
 3167                $this->logger->debug(
"Marking population task for chunk $x $z as orphaned");
 
 3168                $this->activeChunkPopulationTasks[$chunkHash] = 
false;
 
 3172        $this->timings->doChunkUnload->stopTiming();
 
 3181        $spawn = $this->getSpawnLocation();
 
 3182        $spawnX = $spawn->x >> Chunk::COORD_BIT_SIZE;
 
 3183        $spawnZ = $spawn->z >> Chunk::COORD_BIT_SIZE;
 
 3185        return abs($X - $spawnX) <= 1 && abs($Z - $spawnZ) <= 1;
 
 
 3198        $spawn ??= $this->getSpawnLocation();
 
 3203        $this->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, 
null)->onCompletion(
 
 3204            function() use ($spawn, $resolver) : 
void{
 
 3205                $spawn = $this->getSafeSpawn($spawn);
 
 3206                $resolver->resolve($spawn);
 
 3208            function() use ($resolver) : void{
 
 3209                $resolver->reject();
 
 3213        return $resolver->getPromise();
 
 
 3223        if(!($spawn instanceof 
Vector3) || $spawn->y <= $this->minY){
 
 3224            $spawn = $this->getSpawnLocation();
 
 3228        $v = $spawn->floor();
 
 3229        $chunk = $this->getOrLoadChunkAtPosition($v);
 
 3230        if($chunk === 
null){
 
 3231            throw new WorldException(
"Cannot find a safe spawn point in non-generated terrain");
 
 3235        $y = (int) min($max - 2, $v->y);
 
 3236        $wasAir = $this->getBlockAt($x, $y - 1, $z)->getTypeId() === BlockTypeIds::AIR; 
 
 3237        for(; $y > $this->minY; --$y){
 
 3238            if($this->getBlockAt($x, $y, $z)->isFullCube()){
 
 3248        for(; $y >= $this->minY && $y < $max; ++$y){
 
 3249            if(!$this->getBlockAt($x, $y + 1, $z)->isFullCube()){
 
 3250                if(!$this->getBlockAt($x, $y, $z)->isFullCube()){
 
 3251                    return new Position($spawn->x, $y === (
int) $spawn->y ? $spawn->y : $y, $spawn->z, $this);
 
 3258        return new Position($spawn->x, $y, $spawn->z, $this);
 
 
 3272        return $this->time % self::TIME_FULL;
 
 
 3280        return $this->displayName;
 
 
 3289        $this->displayName = $name;
 
 3290        $this->provider->getWorldData()->setName($name);
 
 
 3297        return $this->folderName;
 
 
 3304        $this->time = $time;
 
 
 3312        $this->stopTime = true;
 
 
 3320        $this->stopTime = false;
 
 
 3328        return $this->provider->getWorldData()->getSeed();
 
 
 3339    public function getDifficulty() : int{
 
 3340        return $this->provider->getWorldData()->getDifficulty();
 
 3343    public function setDifficulty(
int $difficulty) : void{
 
 3344        if($difficulty < 0 || $difficulty > 3){
 
 3345            throw new \InvalidArgumentException(
"Invalid difficulty level $difficulty");
 
 3347        (
new WorldDifficultyChangeEvent($this, $this->getDifficulty(), $difficulty))->call();
 
 3348        $this->provider->getWorldData()->setDifficulty($difficulty);
 
 3350        foreach($this->players as $player){
 
 3351            $player->getNetworkSession()->syncWorldDifficulty($this->getDifficulty());
 
 3355    private function addChunkHashToPopulationRequestQueue(
int $chunkHash) : void{
 
 3356        if(!isset($this->chunkPopulationRequestQueueIndex[$chunkHash])){
 
 3357            $this->chunkPopulationRequestQueue->enqueue($chunkHash);
 
 3358            $this->chunkPopulationRequestQueueIndex[$chunkHash] = 
true;
 
 3365    private function enqueuePopulationRequest(
int $chunkX, 
int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
 
 3366        $chunkHash = World::chunkHash($chunkX, $chunkZ);
 
 3367        $this->addChunkHashToPopulationRequestQueue($chunkHash);
 
 3369        $resolver = $this->chunkPopulationRequestMap[$chunkHash] = 
new PromiseResolver();
 
 3370        if($associatedChunkLoader === 
null){
 
 3371            $temporaryLoader = 
new class implements ChunkLoader{};
 
 3372            $this->registerChunkLoader($temporaryLoader, $chunkX, $chunkZ);
 
 3373            $resolver->getPromise()->onCompletion(
 
 3374                fn() => $this->unregisterChunkLoader($temporaryLoader, $chunkX, $chunkZ),
 
 3375                static function() : 
void{}
 
 3378        return $resolver->getPromise();
 
 3381    private function drainPopulationRequestQueue() : void{
 
 3383        while(count($this->activeChunkPopulationTasks) < $this->maxConcurrentChunkPopulationTasks && !$this->chunkPopulationRequestQueue->isEmpty()){
 
 3384            $nextChunkHash = $this->chunkPopulationRequestQueue->dequeue();
 
 3385            unset($this->chunkPopulationRequestQueueIndex[$nextChunkHash]);
 
 3386            World::getXZ($nextChunkHash, $nextChunkX, $nextChunkZ);
 
 3387            if(isset($this->chunkPopulationRequestMap[$nextChunkHash])){
 
 3388                assert(!($this->activeChunkPopulationTasks[$nextChunkHash] ?? 
false), 
"Population for chunk $nextChunkX $nextChunkZ already running");
 
 3390                    !$this->orderChunkPopulation($nextChunkX, $nextChunkZ, 
null)->isResolved() &&
 
 3391                    !isset($this->activeChunkPopulationTasks[$nextChunkHash])
 
 3393                    $failed[] = $nextChunkHash;
 
 3400        foreach($failed as $hash){
 
 3401            $this->addChunkHashToPopulationRequestQueue($hash);
 
 3410    private function checkChunkPopulationPreconditions(
int $chunkX, 
int $chunkZ) : array{
 
 3411        $chunkHash = World::chunkHash($chunkX, $chunkZ);
 
 3412        $resolver = $this->chunkPopulationRequestMap[$chunkHash] ?? 
null;
 
 3413        if($resolver !== 
null && isset($this->activeChunkPopulationTasks[$chunkHash])){
 
 3415            return [$resolver, 
false];
 
 3418        $temporaryChunkLoader = 
new class implements ChunkLoader{};
 
 3419        $this->registerChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
 
 3420        $chunk = $this->loadChunk($chunkX, $chunkZ);
 
 3421        $this->unregisterChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
 
 3422        if($chunk !== 
null && $chunk->isPopulated()){
 
 3424            $resolver ??= 
new PromiseResolver();
 
 3425            unset($this->chunkPopulationRequestMap[$chunkHash]);
 
 3426            $resolver->resolve($chunk);
 
 3427            return [$resolver, 
false];
 
 3429        return [$resolver, 
true];
 
 3444        [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
 
 3445        if(!$proceedWithPopulation){
 
 3446            return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
 
 3449        if(count($this->activeChunkPopulationTasks) >= $this->maxConcurrentChunkPopulationTasks){
 
 3451            return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
 
 3453        return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
 
 
 3467        [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
 
 3468        if(!$proceedWithPopulation){
 
 3469            return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
 
 3472        return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
 
 
 3479    private function internalOrderChunkPopulation(
int $chunkX, 
int $chunkZ, ?ChunkLoader $associatedChunkLoader, ?
PromiseResolver $resolver) : 
Promise{
 
 3480        $chunkHash = 
World::chunkHash($chunkX, $chunkZ);
 
 3482        $timings = $this->timings->chunkPopulationOrder;
 
 3483        $timings->startTiming();
 
 3486            for($xx = -1; $xx <= 1; ++$xx){
 
 3487                for($zz = -1; $zz <= 1; ++$zz){
 
 3488                    if($this->isChunkLocked($chunkX + $xx, $chunkZ + $zz)){
 
 3490                        return $resolver?->
getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
 
 3495            $this->activeChunkPopulationTasks[$chunkHash] = 
true;
 
 3496            if($resolver === 
null){
 
 3497                $resolver = 
new PromiseResolver();
 
 3498                $this->chunkPopulationRequestMap[$chunkHash] = $resolver;
 
 3501            $chunkPopulationLockId = 
new ChunkLockId();
 
 3503            $temporaryChunkLoader = 
new class implements ChunkLoader{
 
 3505            for($xx = -1; $xx <= 1; ++$xx){
 
 3506                for($zz = -1; $zz <= 1; ++$zz){
 
 3507                    $this->lockChunk($chunkX + $xx, $chunkZ + $zz, $chunkPopulationLockId);
 
 3508                    $this->registerChunkLoader($temporaryChunkLoader, $chunkX + $xx, $chunkZ + $zz);
 
 3512            $centerChunk = $this->loadChunk($chunkX, $chunkZ);
 
 3513            $adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
 
 3515            $this->generatorExecutor->populate(
 
 3520                function(Chunk $centerChunk, array $adjacentChunks) use ($chunkPopulationLockId, $chunkX, $chunkZ, $temporaryChunkLoader) : 
void{
 
 3521                    if(!$this->isLoaded()){
 
 3525                    $this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
 
 3531            $timings->stopTiming();
 
 3539    private function generateChunkCallback(ChunkLockId $chunkLockId, 
int $x, 
int $z, Chunk $chunk, array $adjacentChunks, ChunkLoader $temporaryChunkLoader) : void{
 
 3540        $timings = $this->timings->chunkPopulationCompletion;
 
 3541        $timings->startTiming();
 
 3544        for($xx = -1; $xx <= 1; ++$xx){
 
 3545            for($zz = -1; $zz <= 1; ++$zz){
 
 3546                $this->unregisterChunkLoader($temporaryChunkLoader, $x + $xx, $z + $zz);
 
 3547                if(!$this->unlockChunk($x + $xx, $z + $zz, $chunkLockId)){
 
 3553        $index = World::chunkHash($x, $z);
 
 3554        if(!isset($this->activeChunkPopulationTasks[$index])){
 
 3555            throw new AssumptionFailedError(
"This should always be set, regardless of whether the task was orphaned or not");
 
 3557        if(!$this->activeChunkPopulationTasks[$index]){
 
 3558            $this->logger->debug(
"Discarding orphaned population result for chunk x=$x,z=$z");
 
 3559            unset($this->activeChunkPopulationTasks[$index]);
 
 3561            if($dirtyChunks === 0){
 
 3562                $oldChunk = $this->loadChunk($x, $z);
 
 3563                $this->setChunk($x, $z, $chunk);
 
 3565                foreach($adjacentChunks as $relativeChunkHash => $adjacentChunk){
 
 3566                    World::getXZ($relativeChunkHash, $relativeX, $relativeZ);
 
 3567                    if($relativeX < -1 || $relativeX > 1 || $relativeZ < -1 || $relativeZ > 1){
 
 3568                        throw new AssumptionFailedError(
"Adjacent chunks should be in range -1 ... +1 coordinates");
 
 3570                    $this->setChunk($x + $relativeX, $z + $relativeZ, $adjacentChunk);
 
 3573                if(($oldChunk === 
null || !$oldChunk->isPopulated()) && $chunk->isPopulated()){
 
 3574                    if(ChunkPopulateEvent::hasHandlers()){
 
 3575                        (
new ChunkPopulateEvent($this, $x, $z, $chunk))->call();
 
 3578                    foreach($this->getChunkListeners($x, $z) as $listener){
 
 3579                        $listener->onChunkPopulated($x, $z, $chunk);
 
 3583                $this->logger->debug(
"Discarding population result for chunk x=$x,z=$z - terrain was modified on the main thread before async population completed");
 
 3592            unset($this->activeChunkPopulationTasks[$index]);
 
 3594            if($dirtyChunks === 0){
 
 3595                $promise = $this->chunkPopulationRequestMap[$index] ?? 
null;
 
 3596                if($promise !== 
null){
 
 3597                    unset($this->chunkPopulationRequestMap[$index]);
 
 3598                    $promise->resolve($chunk);
 
 3601                    $this->logger->debug(
"Unable to resolve population promise for chunk x=$x,z=$z - populated chunk was forcibly unloaded while setting modified chunks");
 
 3607                $this->addChunkHashToPopulationRequestQueue($index);
 
 3610            $this->drainPopulationRequestQueue();
 
 3612        $timings->stopTiming();
 
 3615    public function doChunkGarbageCollection() : void{
 
 3616        $this->timings->doChunkGC->startTiming();
 
 3618        foreach($this->chunks as $index => $chunk){
 
 3619            if(!isset($this->unloadQueue[$index])){
 
 3620                World::getXZ($index, $X, $Z);
 
 3621                if(!$this->isSpawnChunk($X, $Z)){
 
 3622                    $this->unloadChunkRequest($X, $Z, 
true);
 
 3625            $chunk->collectGarbage();
 
 3628        $this->provider->doGarbageCollection();
 
 3630        $this->timings->doChunkGC->stopTiming();
 
 3633    public function unloadChunks(
bool $force = 
false) : void{
 
 3634        if(count($this->unloadQueue) > 0){
 
 3636            $now = microtime(
true);
 
 3637            foreach($this->unloadQueue as $index => $time){
 
 3638                World::getXZ($index, $X, $Z);
 
 3641                    if($maxUnload <= 0){
 
 3643                    }elseif($time > ($now - 30)){
 
 3649                if($this->unloadChunk($X, $Z, 
true)){
 
 3650                    unset($this->unloadQueue[$index]);