149 private static int $worldIdCounter = 1;
151 public const Y_MAX = 320;
152 public const Y_MIN = -64;
154 public const TIME_DAY = 1000;
155 public const TIME_NOON = 6000;
156 public const TIME_SUNSET = 12000;
157 public const TIME_NIGHT = 13000;
158 public const TIME_MIDNIGHT = 18000;
159 public const TIME_SUNRISE = 23000;
161 public const TIME_FULL = 24000;
163 public const DIFFICULTY_PEACEFUL = 0;
164 public const DIFFICULTY_EASY = 1;
165 public const DIFFICULTY_NORMAL = 2;
166 public const DIFFICULTY_HARD = 3;
168 public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3;
174 private array $players = [];
180 private array $entities = [];
185 private array $entityLastKnownPositions = [];
191 private array $entitiesByChunk = [];
197 public array $updateEntities = [];
199 private bool $inDynamicStateRecalculation =
false;
204 private array $blockCache = [];
209 private array $blockCollisionBoxCache = [];
211 private int $sendTimeTicker = 0;
213 private int $worldId;
215 private int $providerGarbageCollectionTicker = 0;
224 private array $registeredTickingChunks = [];
232 private array $validTickingChunks = [];
239 private array $recheckTickingChunks = [];
245 private array $chunkLoaders = [];
251 private array $chunkListeners = [];
256 private array $playerChunkListeners = [];
262 private array $packetBuffersByChunk = [];
268 private array $unloadQueue = [];
271 public bool $stopTime =
false;
273 private float $sunAnglePercentage = 0.0;
274 private int $skyLightReduction = 0;
276 private string $folderName;
277 private string $displayName;
283 private array $chunks = [];
289 private array $changedBlocks = [];
297 private array $scheduledBlockUpdateQueueIndex = [];
300 private \SplQueue $neighbourBlockUpdateQueue;
305 private array $neighbourBlockUpdateQueueIndex = [];
311 private array $activeChunkPopulationTasks = [];
316 private array $chunkLock = [];
317 private int $maxConcurrentChunkPopulationTasks = 2;
322 private array $chunkPopulationRequestMap = [];
327 private \SplQueue $chunkPopulationRequestQueue;
332 private array $chunkPopulationRequestQueueIndex = [];
338 private array $generatorRegisteredWorkers = [];
340 private bool $autoSave =
true;
342 private int $sleepTicks = 0;
344 private int $chunkTickRadius;
345 private int $tickedBlocksPerSubchunkPerTick = self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK;
350 private array $randomTickBlocks = [];
354 public float $tickRateTime = 0;
356 private bool $doingTick =
false;
359 private string $generator;
361 private bool $unloaded =
false;
366 private array $unloadCallbacks = [];
371 private \Logger $logger;
377 return morton2d_encode($x, $z);
380 private const MORTON3D_BIT_SIZE = 21;
381 private const BLOCKHASH_Y_BITS = 9;
382 private const BLOCKHASH_Y_PADDING = 64;
383 private const BLOCKHASH_Y_OFFSET = self::BLOCKHASH_Y_PADDING - self::Y_MIN;
384 private const BLOCKHASH_Y_MASK = (1 << self::BLOCKHASH_Y_BITS) - 1;
385 private const BLOCKHASH_XZ_MASK = (1 << self::MORTON3D_BIT_SIZE) - 1;
386 private const BLOCKHASH_XZ_EXTRA_BITS = (self::MORTON3D_BIT_SIZE - self::BLOCKHASH_Y_BITS) >> 1;
387 private const BLOCKHASH_XZ_EXTRA_MASK = (1 << self::BLOCKHASH_XZ_EXTRA_BITS) - 1;
388 private const BLOCKHASH_XZ_SIGN_SHIFT = 64 - self::MORTON3D_BIT_SIZE - self::BLOCKHASH_XZ_EXTRA_BITS;
389 private const BLOCKHASH_X_SHIFT = self::BLOCKHASH_Y_BITS;
390 private const BLOCKHASH_Z_SHIFT = self::BLOCKHASH_X_SHIFT + self::BLOCKHASH_XZ_EXTRA_BITS;
395 public static function blockHash(
int $x,
int $y,
int $z) : int{
396 $shiftedY = $y + self::BLOCKHASH_Y_OFFSET;
397 if(($shiftedY & (~0 << self::BLOCKHASH_Y_BITS)) !== 0){
398 throw new \InvalidArgumentException(
"Y coordinate $y is out of range!");
403 return morton3d_encode(
404 $x & self::BLOCKHASH_XZ_MASK,
406 ((($x >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_X_SHIFT) |
407 ((($z >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_Z_SHIFT),
408 $z & self::BLOCKHASH_XZ_MASK
416 return morton3d_encode($x, $y, $z);
425 public static function getBlockXYZ(
int $hash, ?
int &$x, ?
int &$y, ?
int &$z) : void{
426 [$baseX, $baseY, $baseZ] = morton3d_decode($hash);
428 $extraX = ((($baseY >> self::BLOCKHASH_X_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
429 $extraZ = ((($baseY >> self::BLOCKHASH_Z_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
431 $x = (($baseX & self::BLOCKHASH_XZ_MASK) | $extraX) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
432 $y = ($baseY & self::BLOCKHASH_Y_MASK) - self::BLOCKHASH_Y_OFFSET;
433 $z = (($baseZ & self::BLOCKHASH_XZ_MASK) | $extraZ) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
441 public static function getXZ(
int $hash, ?
int &$x, ?
int &$z) : void{
442 [$x, $z] = morton2d_decode($hash);
445 public static function getDifficultyFromString(
string $str) : int{
446 switch(strtolower(trim($str))){
450 return World::DIFFICULTY_PEACEFUL;
455 return World::DIFFICULTY_EASY;
460 return World::DIFFICULTY_NORMAL;
465 return World::DIFFICULTY_HARD;
480 $this->folderName = $name;
481 $this->worldId = self::$worldIdCounter++;
483 $this->displayName = $this->provider->getWorldData()->getName();
484 $this->logger = new \PrefixedLogger($server->getLogger(),
"World: $this->displayName");
486 $this->minY = $this->provider->getWorldMinY();
487 $this->maxY = $this->provider->getWorldMaxY();
489 $this->
server->getLogger()->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_preparing($this->displayName)));
490 $generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
491 throw new AssumptionFailedError(
"WorldManager should already have checked that the generator exists");
492 $generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
493 $this->generator = $generator->getGeneratorClass();
494 $this->chunkPopulationRequestQueue = new \SplQueue();
495 $this->addOnUnloadCallback(
function() :
void{
496 $this->logger->debug(
"Cancelling unfulfilled generation requests");
498 foreach($this->chunkPopulationRequestMap as $chunkHash => $promise){
500 unset($this->chunkPopulationRequestMap[$chunkHash]);
502 if(count($this->chunkPopulationRequestMap) !== 0){
510 $this->scheduledBlockUpdateQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
512 $this->neighbourBlockUpdateQueue = new \SplQueue();
514 $this->time = $this->provider->getWorldData()->getTime();
516 $cfg = $this->
server->getConfigGroup();
517 $this->chunkTickRadius = min($this->
server->getViewDistance(), max(0, $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_TICK_RADIUS, 4)));
518 if($cfg->getPropertyInt(
"chunk-ticking.per-tick", 40) <= 0){
520 $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.");
521 $this->chunkTickRadius = 0;
523 $this->tickedBlocksPerSubchunkPerTick = $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_BLOCKS_PER_SUBCHUNK_PER_TICK, self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK);
524 $this->maxConcurrentChunkPopulationTasks = $cfg->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2);
526 $this->initRandomTickBlocksFromConfig($cfg);
530 $this->workerPool->addWorkerStartHook($workerStartHook =
function(
int $workerId) :
void{
531 if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){
532 $this->logger->debug(
"Worker $workerId with previously registered generator restarted, flagging as unregistered");
533 unset($this->generatorRegisteredWorkers[$workerId]);
536 $workerPool = $this->workerPool;
537 $this->addOnUnloadCallback(
static function() use ($workerPool, $workerStartHook) :
void{
543 $dontTickBlocks = [];
544 $parser = StringToItemParser::getInstance();
545 foreach($cfg->getProperty(YmlServerProperties::CHUNK_TICKING_DISABLE_BLOCK_TICKING, []) as $name){
546 $name = (string) $name;
547 $item = $parser->parse($name);
549 $block = $item->getBlock();
550 }elseif(preg_match(
"/^-?\d+$/", $name) === 1){
553 $blockStateData = GlobalBlockStateHandlers::getUpgrader()->upgradeIntIdMeta((
int) $name, 0);
554 }
catch(BlockStateDeserializeException){
557 $block = RuntimeBlockStateRegistry::getInstance()->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData));
563 if($block->getTypeId() !== BlockTypeIds::AIR){
564 $dontTickBlocks[$block->getTypeId()] = $name;
568 foreach(RuntimeBlockStateRegistry::getInstance()->getAllKnownStates() as $state){
569 $dontTickName = $dontTickBlocks[$state->getTypeId()] ??
null;
570 if($dontTickName ===
null && $state->ticksRandomly()){
571 $this->randomTickBlocks[$state->getStateId()] =
true;
576 public function getTickRateTime() : float{
577 return $this->tickRateTime;
580 public function registerGeneratorToWorker(
int $worker) : void{
581 $this->logger->debug(
"Registering generator on worker $worker");
582 $this->workerPool->submitTaskToWorker(
new GeneratorRegisterTask($this, $this->generator, $this->provider->getWorldData()->getGeneratorOptions()), $worker);
583 $this->generatorRegisteredWorkers[$worker] =
true;
586 public function unregisterGenerator() : void{
587 foreach($this->workerPool->getRunningWorkers() as $i){
588 if(isset($this->generatorRegisteredWorkers[$i])){
589 $this->workerPool->submitTaskToWorker(
new GeneratorUnregisterTask($this), $i);
592 $this->generatorRegisteredWorkers = [];
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 foreach($this->entitiesByChunk as $chunkHash => $entities){
636 self::getXZ($chunkHash, $chunkX, $chunkZ);
639 foreach($entities as $entity){
640 if(!$entity->isFlaggedForDespawn()){
645 if($leakedEntities !== 0){
646 $this->logger->warning(
"$leakedEntities leaked entities found in ungenerated chunk $chunkX $chunkZ during unload, they won't be saved!");
652 $this->unregisterGenerator();
654 $this->provider->close();
655 $this->blockCache = [];
656 $this->blockCollisionBoxCache = [];
658 $this->unloaded =
true;
663 $this->unloadCallbacks[spl_object_id($callback)] = $callback;
668 unset($this->unloadCallbacks[spl_object_id($callback)]);
680 private function filterViewersForPosition(
Vector3 $pos, array $allowed) : array{
681 $candidates = $this->getViewersForPosition($pos);
683 foreach($allowed as $player){
684 $k = spl_object_id($player);
685 if(isset($candidates[$k])){
686 $filtered[$k] = $candidates[$k];
697 $players ??= $this->getViewersForPosition($pos);
699 if(WorldSoundEvent::hasHandlers()){
702 if($ev->isCancelled()){
706 $sound = $ev->getSound();
707 $players = $ev->getRecipients();
710 $pk = $sound->
encode($pos);
712 if($players === $this->getViewersForPosition($pos)){
714 $this->broadcastPacketToViewers($pos, $e);
717 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
726 $players ??= $this->getViewersForPosition($pos);
728 if(WorldParticleEvent::hasHandlers()){
731 if($ev->isCancelled()){
735 $particle = $ev->getParticle();
736 $players = $ev->getRecipients();
739 $pk = $particle->
encode($pos);
741 if($players === $this->getViewersForPosition($pos)){
743 $this->broadcastPacketToViewers($pos, $e);
746 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
751 public function getAutoSave() : bool{
752 return $this->autoSave;
755 public function setAutoSave(
bool $value) : void{
756 $this->autoSave = $value;
769 return $this->playerChunkListeners[
World::chunkHash($chunkX, $chunkZ)] ?? [];
779 return $this->chunkLoaders[
World::chunkHash($chunkX, $chunkZ)] ?? [];
789 return $this->getChunkPlayers($pos->getFloorX() >>
Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >>
Chunk::COORD_BIT_SIZE);
796 $this->broadcastPacketToPlayersUsingChunk($pos->getFloorX() >>
Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >>
Chunk::COORD_BIT_SIZE, $packet);
799 private function broadcastPacketToPlayersUsingChunk(
int $chunkX,
int $chunkZ,
ClientboundPacket $packet) : void{
800 if(!isset($this->packetBuffersByChunk[$index =
World::chunkHash($chunkX, $chunkZ)])){
801 $this->packetBuffersByChunk[$index] = [$packet];
803 $this->packetBuffersByChunk[$index][] = $packet;
807 public function registerChunkLoader(ChunkLoader $loader,
int $chunkX,
int $chunkZ,
bool $autoLoad =
true) : void{
808 $loaderId = spl_object_id($loader);
810 if(!isset($this->chunkLoaders[$chunkHash = World::chunkHash($chunkX, $chunkZ)])){
811 $this->chunkLoaders[$chunkHash] = [];
812 }elseif(isset($this->chunkLoaders[$chunkHash][$loaderId])){
816 $this->chunkLoaders[$chunkHash][$loaderId] = $loader;
818 $this->cancelUnloadChunkRequest($chunkX, $chunkZ);
821 $this->loadChunk($chunkX, $chunkZ);
825 public function unregisterChunkLoader(ChunkLoader $loader,
int $chunkX,
int $chunkZ) : void{
826 $chunkHash = World::chunkHash($chunkX, $chunkZ);
827 $loaderId = spl_object_id($loader);
828 if(isset($this->chunkLoaders[$chunkHash][$loaderId])){
829 if(count($this->chunkLoaders[$chunkHash]) === 1){
830 unset($this->chunkLoaders[$chunkHash]);
831 $this->unloadChunkRequest($chunkX, $chunkZ,
true);
832 if(isset($this->chunkPopulationRequestMap[$chunkHash]) && !isset($this->activeChunkPopulationTasks[$chunkHash])){
833 $this->chunkPopulationRequestMap[$chunkHash]->reject();
834 unset($this->chunkPopulationRequestMap[$chunkHash]);
837 unset($this->chunkLoaders[$chunkHash][$loaderId]);
846 $hash =
World::chunkHash($chunkX, $chunkZ);
847 if(isset($this->chunkListeners[$hash])){
848 $this->chunkListeners[$hash][spl_object_id($listener)] = $listener;
850 $this->chunkListeners[$hash] = [spl_object_id($listener) => $listener];
852 if($listener instanceof
Player){
853 $this->playerChunkListeners[$hash][spl_object_id($listener)] = $listener;
863 $hash =
World::chunkHash($chunkX, $chunkZ);
864 if(isset($this->chunkListeners[$hash])){
865 if(count($this->chunkListeners[$hash]) === 1){
866 unset($this->chunkListeners[$hash]);
867 unset($this->playerChunkListeners[$hash]);
869 unset($this->chunkListeners[$hash][spl_object_id($listener)]);
870 unset($this->playerChunkListeners[$hash][spl_object_id($listener)]);
879 foreach($this->chunkListeners as $hash => $listeners){
880 World::getXZ($hash, $chunkX, $chunkZ);
881 $this->unregisterChunkListener($listener, $chunkX, $chunkZ);
892 return $this->chunkListeners[
World::chunkHash($chunkX, $chunkZ)] ?? [];
898 public function sendTime(
Player ...$targets) : void{
899 if(count($targets) === 0){
900 $targets = $this->players;
902 foreach($targets as $player){
903 $player->getNetworkSession()->syncWorldTime($this->time);
907 public function isDoingTick() : bool{
908 return $this->doingTick;
914 public function doTick(
int $currentTick) : void{
916 throw new \LogicException(
"Attempted to tick a world which has been closed");
919 $this->timings->doTick->startTiming();
920 $this->doingTick =
true;
922 $this->actuallyDoTick($currentTick);
924 $this->doingTick =
false;
925 $this->timings->doTick->stopTiming();
929 protected function actuallyDoTick(
int $currentTick) : void{
930 if(!$this->stopTime){
932 if($this->time === PHP_INT_MAX){
933 $this->time = PHP_INT_MIN;
939 $this->sunAnglePercentage = $this->computeSunAnglePercentage();
940 $this->skyLightReduction = $this->computeSkyLightReduction();
942 if(++$this->sendTimeTicker === 200){
944 $this->sendTimeTicker = 0;
947 $this->unloadChunks();
948 if(++$this->providerGarbageCollectionTicker >= 6000){
949 $this->provider->doGarbageCollection();
950 $this->providerGarbageCollectionTicker = 0;
953 $this->timings->scheduledBlockUpdates->startTiming();
955 while($this->scheduledBlockUpdateQueue->count() > 0 && $this->scheduledBlockUpdateQueue->current()[
"priority"] <= $currentTick){
957 $vec = $this->scheduledBlockUpdateQueue->extract()[
"data"];
958 unset($this->scheduledBlockUpdateQueueIndex[World::blockHash($vec->x, $vec->y, $vec->z)]);
959 if(!$this->isInLoadedTerrain($vec)){
962 $block = $this->getBlock($vec);
963 $block->onScheduledUpdate();
965 $this->timings->scheduledBlockUpdates->stopTiming();
967 $this->timings->neighbourBlockUpdates->startTiming();
969 while($this->neighbourBlockUpdateQueue->count() > 0){
970 $index = $this->neighbourBlockUpdateQueue->dequeue();
971 unset($this->neighbourBlockUpdateQueueIndex[$index]);
972 World::getBlockXYZ($index, $x, $y, $z);
973 if(!$this->isChunkLoaded($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)){
977 $block = $this->getBlockAt($x, $y, $z);
979 if(BlockUpdateEvent::hasHandlers()){
980 $ev =
new BlockUpdateEvent($block);
982 if($ev->isCancelled()){
986 foreach($this->getNearbyEntities(AxisAlignedBB::one()->offset($x, $y, $z)) as $entity){
987 $entity->onNearbyBlockChange();
989 $block->onNearbyBlockChange();
992 $this->timings->neighbourBlockUpdates->stopTiming();
994 $this->timings->entityTick->startTiming();
996 foreach($this->updateEntities as $id => $entity){
997 if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->onUpdate($currentTick)){
998 unset($this->updateEntities[$id]);
1000 if($entity->isFlaggedForDespawn()){
1004 $this->timings->entityTick->stopTiming();
1006 $this->timings->randomChunkUpdates->startTiming();
1007 $this->tickChunks();
1008 $this->timings->randomChunkUpdates->stopTiming();
1010 $this->executeQueuedLightUpdates();
1012 if(count($this->changedBlocks) > 0){
1013 if(count($this->players) > 0){
1014 foreach($this->changedBlocks as $index => $blocks){
1015 if(count($blocks) === 0){
1018 World::getXZ($index, $chunkX, $chunkZ);
1019 if(!$this->isChunkLoaded($chunkX, $chunkZ)){
1023 if(count($blocks) > 512){
1024 $chunk = $this->getChunk($chunkX, $chunkZ) ??
throw new AssumptionFailedError(
"We already checked that the chunk is loaded");
1025 foreach($this->getChunkPlayers($chunkX, $chunkZ) as $p){
1026 $p->onChunkChanged($chunkX, $chunkZ, $chunk);
1029 foreach($this->createBlockUpdatePackets($blocks) as $packet){
1030 $this->broadcastPacketToPlayersUsingChunk($chunkX, $chunkZ, $packet);
1036 $this->changedBlocks = [];
1040 if($this->sleepTicks > 0 && --$this->sleepTicks <= 0){
1041 $this->checkSleep();
1044 foreach($this->packetBuffersByChunk as $index => $entries){
1045 World::getXZ($index, $chunkX, $chunkZ);
1046 $chunkPlayers = $this->getChunkPlayers($chunkX, $chunkZ);
1047 if(count($chunkPlayers) > 0){
1048 NetworkBroadcastUtils::broadcastPackets($chunkPlayers, $entries);
1052 $this->packetBuffersByChunk = [];
1055 public function checkSleep() : void{
1056 if(count($this->players) === 0){
1061 foreach($this->getPlayers() as $p){
1062 if(!$p->isSleeping()){
1069 $time = $this->getTimeOfDay();
1071 if($time >= World::TIME_NIGHT && $time < World::TIME_SUNRISE){
1072 $this->setTime($this->getTime() + World::TIME_FULL - $time);
1074 foreach($this->getPlayers() as $p){
1081 public function setSleepTicks(
int $ticks) : void{
1082 $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->blockCollisionBoxCache = [];
1144 foreach($this->blockCache as $list){
1145 $count += count($list);
1147 $this->blockCache = [];
1153 foreach($this->blockCollisionBoxCache as $list){
1154 $count += count($list);
1157 $this->blockCollisionBoxCache = [];
1169 return $this->randomTickBlocks;
1172 public function addRandomTickedBlock(
Block $block) : void{
1174 throw new \InvalidArgumentException(
"Cannot do random-tick on unknown block");
1176 $this->randomTickBlocks[$block->getStateId()] =
true;
1179 public function removeRandomTickedBlock(Block $block) : void{
1180 unset($this->randomTickBlocks[$block->getStateId()]);
1188 return $this->chunkTickRadius;
1196 $this->chunkTickRadius = $radius;
1207 return array_keys($this->validTickingChunks);
1215 $chunkPosHash =
World::chunkHash($chunkX, $chunkZ);
1216 $this->registeredTickingChunks[$chunkPosHash][spl_object_id($ticker)] = $ticker;
1217 $this->recheckTickingChunks[$chunkPosHash] = $chunkPosHash;
1225 $chunkHash =
World::chunkHash($chunkX, $chunkZ);
1226 $tickerId = spl_object_id($ticker);
1227 if(isset($this->registeredTickingChunks[$chunkHash][$tickerId])){
1228 if(count($this->registeredTickingChunks[$chunkHash]) === 1){
1230 $this->registeredTickingChunks[$chunkHash],
1231 $this->recheckTickingChunks[$chunkHash],
1232 $this->validTickingChunks[$chunkHash]
1235 unset($this->registeredTickingChunks[$chunkHash][$tickerId]);
1240 private function tickChunks() : void{
1241 if($this->chunkTickRadius <= 0 || count($this->registeredTickingChunks) === 0){
1245 if(count($this->recheckTickingChunks) > 0){
1246 $this->timings->randomChunkUpdatesChunkSelection->startTiming();
1248 $chunkTickableCache = [];
1250 foreach($this->recheckTickingChunks as $hash => $_){
1251 World::getXZ($hash, $chunkX, $chunkZ);
1252 if($this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){
1253 $this->validTickingChunks[$hash] = $hash;
1256 $this->recheckTickingChunks = [];
1258 $this->timings->randomChunkUpdatesChunkSelection->stopTiming();
1261 foreach($this->validTickingChunks as $index => $_){
1262 World::getXZ($index, $chunkX, $chunkZ);
1264 $this->tickChunk($chunkX, $chunkZ);
1274 private function isChunkTickable(
int $chunkX,
int $chunkZ, array &$cache) : bool{
1275 for($cx = -1; $cx <= 1; ++$cx){
1276 for($cz = -1; $cz <= 1; ++$cz){
1277 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1278 if(isset($cache[$chunkHash])){
1279 if(!$cache[$chunkHash]){
1284 if($this->isChunkLocked($chunkX + $cx, $chunkZ + $cz)){
1285 $cache[$chunkHash] =
false;
1288 $adjacentChunk = $this->getChunk($chunkX + $cx, $chunkZ + $cz);
1289 if($adjacentChunk ===
null || !$adjacentChunk->isPopulated()){
1290 $cache[$chunkHash] =
false;
1293 $lightPopulatedState = $adjacentChunk->isLightPopulated();
1294 if($lightPopulatedState !==
true){
1295 if($lightPopulatedState ===
false){
1296 $this->orderLightPopulation($chunkX + $cx, $chunkZ + $cz);
1298 $cache[$chunkHash] =
false;
1302 $cache[$chunkHash] =
true;
1318 private function markTickingChunkForRecheck(
int $chunkX,
int $chunkZ) : void{
1319 for($cx = -1; $cx <= 1; ++$cx){
1320 for($cz = -1; $cz <= 1; ++$cz){
1321 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1322 unset($this->validTickingChunks[$chunkHash]);
1323 if(isset($this->registeredTickingChunks[$chunkHash])){
1324 $this->recheckTickingChunks[$chunkHash] = $chunkHash;
1326 unset($this->recheckTickingChunks[$chunkHash]);
1332 private function orderLightPopulation(
int $chunkX,
int $chunkZ) : void{
1333 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1334 $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated();
1335 if($lightPopulatedState ===
false){
1336 $this->chunks[$chunkHash]->setLightPopulated(
null);
1337 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1339 $this->workerPool->submitTask(
new LightPopulationTask(
1340 $this->chunks[$chunkHash],
1341 function(array $blockLight, array $skyLight, array $heightMap) use ($chunkX, $chunkZ) :
void{
1348 if($this->unloaded || ($chunk = $this->getChunk($chunkX, $chunkZ)) ===
null || $chunk->isLightPopulated() ===
true){
1353 $chunk->setHeightMapArray($heightMap);
1354 foreach($blockLight as $y => $lightArray){
1355 $chunk->getSubChunk($y)->setBlockLightArray($lightArray);
1357 foreach($skyLight as $y => $lightArray){
1358 $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray);
1360 $chunk->setLightPopulated(
true);
1361 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1367 private function tickChunk(
int $chunkX,
int $chunkZ) : void{
1368 $chunk = $this->getChunk($chunkX, $chunkZ);
1369 if($chunk ===
null){
1373 foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){
1374 $entity->onRandomUpdate();
1377 $blockFactory = RuntimeBlockStateRegistry::getInstance();
1378 foreach($chunk->getSubChunks() as $Y => $subChunk){
1379 if(!$subChunk->isEmptyFast()){
1381 for($i = 0; $i < $this->tickedBlocksPerSubchunkPerTick; ++$i){
1384 $k = mt_rand(0, (1 << 60) - 1);
1386 $x = $k & SubChunk::COORD_MASK;
1387 $y = ($k >> SubChunk::COORD_BIT_SIZE) & SubChunk::COORD_MASK;
1388 $z = ($k >> (SubChunk::COORD_BIT_SIZE * 2)) & SubChunk::COORD_MASK;
1389 $k >>= (SubChunk::COORD_BIT_SIZE * 3);
1391 $state = $subChunk->getBlockStateId($x, $y, $z);
1393 if(isset($this->randomTickBlocks[$state])){
1394 $block = $blockFactory->fromStateId($state);
1395 $block->position($this, $chunkX * Chunk::EDGE_LENGTH + $x, ($Y << SubChunk::COORD_BIT_SIZE) + $y, $chunkZ * Chunk::EDGE_LENGTH + $z);
1396 $block->onRandomTick();
1410 public function save(
bool $force =
false) : bool{
1412 if(!$this->getAutoSave() && !$force){
1416 (
new WorldSaveEvent($this))->call();
1418 $timings = $this->timings->syncDataSave;
1419 $timings->startTiming();
1421 $this->provider->getWorldData()->setTime($this->time);
1422 $this->saveChunks();
1423 $this->provider->getWorldData()->save();
1425 $timings->stopTiming();
1430 public function saveChunks() : void{
1431 $this->timings->syncChunkSave->startTiming();
1433 foreach($this->chunks as $chunkHash => $chunk){
1434 self::getXZ($chunkHash, $chunkX, $chunkZ);
1435 $this->provider->saveChunk($chunkX, $chunkZ,
new ChunkData(
1436 $chunk->getSubChunks(),
1437 $chunk->isPopulated(),
1438 array_map(fn(Entity $e) => $e->saveNBT(), array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk())),
1439 array_map(fn(Tile $t) => $t->saveNBT(), $chunk->getTiles()),
1440 ), $chunk->getTerrainDirtyFlags());
1441 $chunk->clearTerrainDirtyFlags();
1444 $this->timings->syncChunkSave->stopTiming();
1454 !$this->isInWorld($pos->x, $pos->y, $pos->z) ||
1455 (isset($this->scheduledBlockUpdateQueueIndex[$index =
World::blockHash($pos->x, $pos->y, $pos->z)]) && $this->scheduledBlockUpdateQueueIndex[$index] <= $delay)
1459 $this->scheduledBlockUpdateQueueIndex[$index] = $delay;
1460 $this->scheduledBlockUpdateQueue->insert(
new Vector3((
int) $pos->x, (
int) $pos->y, (
int) $pos->z), $delay + $this->server->getTick());
1463 private function tryAddToNeighbourUpdateQueue(
int $x,
int $y,
int $z) : void{
1464 if($this->isInWorld($x, $y, $z)){
1465 $hash = World::blockHash($x, $y, $z);
1466 if(!isset($this->neighbourBlockUpdateQueueIndex[$hash])){
1467 $this->neighbourBlockUpdateQueue->enqueue($hash);
1468 $this->neighbourBlockUpdateQueueIndex[$hash] =
true;
1479 private function internalNotifyNeighbourBlockUpdate(
int $x,
int $y,
int $z) : void{
1480 $this->tryAddToNeighbourUpdateQueue($x, $y, $z);
1481 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1482 $this->tryAddToNeighbourUpdateQueue($x + $dx, $y + $dy, $z + $dz);
1494 $this->internalNotifyNeighbourBlockUpdate($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ());
1502 $minX = (int) floor($bb->minX - 1);
1503 $minY = (int) floor($bb->minY - 1);
1504 $minZ = (int) floor($bb->minZ - 1);
1505 $maxX = (int) floor($bb->maxX + 1);
1506 $maxY = (int) floor($bb->maxY + 1);
1507 $maxZ = (int) floor($bb->maxZ + 1);
1512 for($z = $minZ; $z <= $maxZ; ++$z){
1513 for($x = $minX; $x <= $maxX; ++$x){
1514 for($y = $minY; $y <= $maxY; ++$y){
1515 $block = $this->getBlockAt($x, $y, $z);
1516 if($block->collidesWithBB($bb)){
1523 for($z = $minZ; $z <= $maxZ; ++$z){
1524 for($x = $minX; $x <= $maxX; ++$x){
1525 for($y = $minY; $y <= $maxY; ++$y){
1526 $block = $this->getBlockAt($x, $y, $z);
1527 if($block->collidesWithBB($bb)){
1528 $collides[] = $block;
1545 private function getBlockCollisionBoxesForCell(
int $x,
int $y,
int $z) : array{
1546 $block = $this->getBlockAt($x, $y, $z);
1547 $boxes = $block->getCollisionBoxes();
1549 $cellBB = AxisAlignedBB::one()->offset($x, $y, $z);
1550 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1551 $extraBoxes = $this->getBlockAt($x + $dx, $y + $dy, $z + $dz)->getCollisionBoxes();
1552 foreach($extraBoxes as $extraBox){
1553 if($extraBox->intersectsWith($cellBB)){
1554 $boxes[] = $extraBox;
1567 $minX = (int) floor($bb->minX);
1568 $minY = (int) floor($bb->minY);
1569 $minZ = (int) floor($bb->minZ);
1570 $maxX = (int) floor($bb->maxX);
1571 $maxY = (int) floor($bb->maxY);
1572 $maxZ = (int) floor($bb->maxZ);
1576 for($z = $minZ; $z <= $maxZ; ++$z){
1577 for($x = $minX; $x <= $maxX; ++$x){
1578 $chunkPosHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1579 for($y = $minY; $y <= $maxY; ++$y){
1580 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1582 $boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z);
1584 foreach($boxes as $blockBB){
1585 if($blockBB->intersectsWith($bb)){
1586 $collides[] = $blockBB;
1604 $collides = $this->getBlockCollisionBoxes($bb);
1607 foreach($this->getCollidingEntities($bb->
expandedCopy(0.25, 0.25, 0.25), $entity) as $ent){
1608 $collides[] = clone $ent->boundingBox;
1620 $timeProgress = ($this->time % self::TIME_FULL) / self::TIME_FULL;
1623 $sunProgress = $timeProgress + ($timeProgress < 0.25 ? 0.75 : -0.25);
1627 $diff = (((1 - ((cos($sunProgress * M_PI) + 1) / 2)) - $sunProgress) / 3);
1629 return $sunProgress + $diff;
1636 return $this->sunAnglePercentage;
1643 return $this->sunAnglePercentage * 2 * M_PI;
1650 return $this->sunAnglePercentage * 360.0;
1658 $percentage = max(0, min(1, -(cos($this->getSunAngleRadians()) * 2 - 0.5)));
1662 return (
int) ($percentage * 11);
1669 return $this->skyLightReduction;
1677 $floorX = $pos->getFloorX();
1678 $floorY = $pos->getFloorY();
1679 $floorZ = $pos->getFloorZ();
1680 return $this->getFullLightAt($floorX, $floorY, $floorZ);
1688 $skyLight = $this->getRealBlockSkyLightAt($x, $y, $z);
1690 return max($skyLight, $this->getBlockLightAt($x, $y, $z));
1701 return $this->getHighestAdjacentLight($x, $y, $z, $this->getFullLightAt(...));
1709 $floorX = $pos->getFloorX();
1710 $floorY = $pos->getFloorY();
1711 $floorZ = $pos->getFloorZ();
1712 return $this->getPotentialLightAt($floorX, $floorY, $floorZ);
1720 return max($this->getPotentialBlockSkyLightAt($x, $y, $z), $this->getBlockLightAt($x, $y, $z));
1728 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialLightAt(...));
1738 if(!$this->isInWorld($x, $y, $z)){
1739 return $y >= self::Y_MAX ? 15 : 0;
1741 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !==
null && $chunk->isLightPopulated() ===
true){
1742 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockSkyLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1753 $light = $this->getPotentialBlockSkyLightAt($x, $y, $z) - $this->skyLightReduction;
1754 return $light < 0 ? 0 : $light;
1763 if(!$this->isInWorld($x, $y, $z)){
1766 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !==
null && $chunk->isLightPopulated() ===
true){
1767 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1772 public function updateAllLight(
int $x,
int $y,
int $z) : void{
1773 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) === null || $chunk->isLightPopulated() !== true){
1777 $blockFactory = RuntimeBlockStateRegistry::getInstance();
1778 $this->timings->doBlockSkyLightUpdates->startTiming();
1779 if($this->skyLightUpdate ===
null){
1780 $this->skyLightUpdate =
new SkyLightUpdate(
new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight);
1782 $this->skyLightUpdate->recalculateNode($x, $y, $z);
1783 $this->timings->doBlockSkyLightUpdates->stopTiming();
1785 $this->timings->doBlockLightUpdates->startTiming();
1786 if($this->blockLightUpdate ===
null){
1787 $this->blockLightUpdate =
new BlockLightUpdate(
new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->light);
1789 $this->blockLightUpdate->recalculateNode($x, $y, $z);
1790 $this->timings->doBlockLightUpdates->stopTiming();
1796 private function getHighestAdjacentLight(
int $x,
int $y,
int $z, \Closure $lightGetter) : int{
1798 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
1799 $x1 = $x + $offsetX;
1800 $y1 = $y + $offsetY;
1801 $z1 = $z + $offsetZ;
1803 !$this->isInWorld($x1, $y1, $z1) ||
1804 ($chunk = $this->getChunk($x1 >> Chunk::COORD_BIT_SIZE, $z1 >> Chunk::COORD_BIT_SIZE)) ===
null ||
1805 $chunk->isLightPopulated() !==
true
1809 $max = max($max, $lightGetter($x1, $y1, $z1));
1818 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialBlockSkyLightAt(...));
1826 return $this->getHighestAdjacentPotentialBlockSkyLight($x, $y, $z) - $this->skyLightReduction;
1833 return $this->getHighestAdjacentLight($x, $y, $z, $this->getBlockLightAt(...));
1836 private function executeQueuedLightUpdates() : void{
1837 if($this->blockLightUpdate !== null){
1838 $this->timings->doBlockLightUpdates->startTiming();
1839 $this->blockLightUpdate->execute();
1840 $this->blockLightUpdate =
null;
1841 $this->timings->doBlockLightUpdates->stopTiming();
1844 if($this->skyLightUpdate !==
null){
1845 $this->timings->doBlockSkyLightUpdates->startTiming();
1846 $this->skyLightUpdate->execute();
1847 $this->skyLightUpdate =
null;
1848 $this->timings->doBlockSkyLightUpdates->stopTiming();
1855 $y < $this->maxY && $y >= $this->minY &&
1871 return $this->getBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $cached, $addToCache);
1883 public function getBlockAt(
int $x,
int $y,
int $z,
bool $cached =
true,
bool $addToCache =
true) :
Block{
1884 $relativeBlockHash = null;
1885 $chunkHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1887 if($this->isInWorld($x, $y, $z)){
1888 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1890 if($cached && isset($this->blockCache[$chunkHash][$relativeBlockHash])){
1891 return $this->blockCache[$chunkHash][$relativeBlockHash];
1894 $chunk = $this->chunks[$chunkHash] ??
null;
1895 if($chunk !==
null){
1896 $block = RuntimeBlockStateRegistry::getInstance()->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK));
1898 $addToCache =
false;
1899 $block = VanillaBlocks::AIR();
1902 $block = VanillaBlocks::AIR();
1905 $block->position($this, $x, $y, $z);
1907 if($this->inDynamicStateRecalculation){
1911 $addToCache =
false;
1913 $this->inDynamicStateRecalculation =
true;
1914 $replacement = $block->readStateFromWorld();
1915 if($replacement !== $block){
1916 $replacement->position($this, $x, $y, $z);
1917 $block = $replacement;
1919 $this->inDynamicStateRecalculation =
false;
1922 if($addToCache && $relativeBlockHash !==
null){
1923 $this->blockCache[$chunkHash][$relativeBlockHash] = $block;
1935 $this->setBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $block, $update);
1946 public function setBlockAt(
int $x,
int $y,
int $z,
Block $block,
bool $update =
true) : void{
1947 if(!$this->isInWorld($x, $y, $z)){
1948 throw new \InvalidArgumentException(
"Pos x=$x,y=$y,z=$z is outside of the world bounds");
1950 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
1951 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
1952 if($this->loadChunk($chunkX, $chunkZ) ===
null){
1953 throw new WorldException(
"Cannot set a block in un-generated terrain");
1956 $this->timings->setBlock->startTiming();
1958 $this->unlockChunk($chunkX, $chunkZ,
null);
1960 $block = clone $block;
1962 $block->position($this, $x, $y, $z);
1963 $block->writeStateToWorld();
1964 $pos =
new Vector3($x, $y, $z);
1966 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1967 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1969 unset($this->blockCache[$chunkHash][$relativeBlockHash]);
1970 unset($this->blockCollisionBoxCache[$chunkHash][$relativeBlockHash]);
1973 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
1974 $sideChunkPosHash = World::chunkHash(($x + $offsetX) >> Chunk::COORD_BIT_SIZE, ($z + $offsetZ) >> Chunk::COORD_BIT_SIZE);
1975 $sideChunkBlockHash = World::chunkBlockHash($x + $offsetX, $y + $offsetY, $z + $offsetZ);
1976 unset($this->blockCollisionBoxCache[$sideChunkPosHash][$sideChunkBlockHash]);
1979 if(!isset($this->changedBlocks[$chunkHash])){
1980 $this->changedBlocks[$chunkHash] = [];
1982 $this->changedBlocks[$chunkHash][$relativeBlockHash] = $pos;
1984 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
1985 $listener->onBlockChanged($pos);
1989 $this->updateAllLight($x, $y, $z);
1990 $this->internalNotifyNeighbourBlockUpdate($x, $y, $z);
1993 $this->timings->setBlock->stopTiming();
1996 public function dropItem(Vector3 $source, Item $item, ?Vector3 $motion =
null,
int $delay = 10) : ?ItemEntity{
1997 if($item->isNull()){
2001 $itemEntity =
new ItemEntity(Location::fromObject($source, $this, lcg_value() * 360, 0), $item);
2003 $itemEntity->setPickupDelay($delay);
2004 $itemEntity->setMotion($motion ??
new Vector3(lcg_value() * 0.2 - 0.1, 0.2, lcg_value() * 0.2 - 0.1));
2005 $itemEntity->spawnToAll();
2020 foreach(ExperienceOrb::splitIntoOrbSizes($amount) as $split){
2021 $orb =
new ExperienceOrb(Location::fromObject($pos, $this, lcg_value() * 360, 0), $split);
2023 $orb->setMotion(
new Vector3((lcg_value() * 0.2 - 0.1) * 2, lcg_value() * 0.4, (lcg_value() * 0.2 - 0.1) * 2));
2040 public function useBreakOn(
Vector3 $vector,
Item &$item =
null, ?
Player $player =
null,
bool $createParticles =
false, array &$returnedItems = []) : bool{
2041 $vector = $vector->floor();
2043 $chunkX = $vector->getFloorX() >> Chunk::COORD_BIT_SIZE;
2044 $chunkZ = $vector->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2045 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2049 $target = $this->getBlock($vector);
2050 $affectedBlocks = $target->getAffectedBlocks();
2053 $item = VanillaItems::AIR();
2057 if($player ===
null || $player->hasFiniteResources()){
2058 $drops = array_merge(...array_map(fn(Block $block) => $block->getDrops($item), $affectedBlocks));
2062 if($player !==
null && $player->hasFiniteResources()){
2063 $xpDrop = array_sum(array_map(fn(Block $block) => $block->getXpDropForTool($item), $affectedBlocks));
2066 if($player !==
null){
2067 $ev =
new BlockBreakEvent($player, $target, $item, $player->isCreative(), $drops, $xpDrop);
2069 if($target instanceof Air || ($player->isSurvival() && !$target->getBreakInfo()->isBreakable()) || $player->isSpectator()){
2073 if($player->isAdventure(
true) && !$ev->isCancelled()){
2075 $itemParser = LegacyStringToItemParser::getInstance();
2076 foreach($item->getCanDestroy() as $v){
2077 $entry = $itemParser->parse($v);
2078 if($entry->getBlock()->hasSameTypeId($target)){
2090 if($ev->isCancelled()){
2094 $drops = $ev->getDrops();
2095 $xpDrop = $ev->getXpDropAmount();
2097 }elseif(!$target->getBreakInfo()->isBreakable()){
2101 foreach($affectedBlocks as $t){
2102 $this->destroyBlockInternal($t, $item, $player, $createParticles, $returnedItems);
2105 $item->onDestroyBlock($target, $returnedItems);
2107 if(count($drops) > 0){
2108 $dropPos = $vector->add(0.5, 0.5, 0.5);
2109 foreach($drops as $drop){
2110 if(!$drop->isNull()){
2111 $this->dropItem($dropPos, $drop);
2117 $this->dropExperience($vector->add(0.5, 0.5, 0.5), $xpDrop);
2126 private function destroyBlockInternal(Block $target, Item $item, ?Player $player,
bool $createParticles, array &$returnedItems) : void{
2127 if($createParticles){
2128 $this->addParticle($target->getPosition()->add(0.5, 0.5, 0.5),
new BlockBreakParticle($target));
2131 $target->onBreak($item, $player, $returnedItems);
2133 $tile = $this->getTile($target->getPosition());
2135 $tile->onBlockDestroyed();
2146 public function useItemOn(
Vector3 $vector,
Item &$item,
int $face, ?
Vector3 $clickVector =
null, ?
Player $player =
null,
bool $playSound =
false, array &$returnedItems = []) : bool{
2147 $blockClicked = $this->getBlock($vector);
2148 $blockReplace = $blockClicked->getSide($face);
2150 if($clickVector ===
null){
2151 $clickVector =
new Vector3(0.0, 0.0, 0.0);
2154 min(1.0, max(0.0, $clickVector->x)),
2155 min(1.0, max(0.0, $clickVector->y)),
2156 min(1.0, max(0.0, $clickVector->z))
2160 if(!$this->isInWorld($blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z)){
2164 $chunkX = $blockReplace->getPosition()->getFloorX() >> Chunk::COORD_BIT_SIZE;
2165 $chunkZ = $blockReplace->getPosition()->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2166 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2170 if($blockClicked->getTypeId() === BlockTypeIds::AIR){
2174 if($player !==
null){
2175 $ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK);
2176 if($player->isSpectator()){
2181 if(!$ev->isCancelled()){
2182 if((!$player->isSneaking() || $item->isNull()) && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2186 $result = $item->
onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems);
2187 if($result !== ItemUseResult::NONE){
2188 return $result === ItemUseResult::SUCCESS;
2193 }elseif($blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2197 if($item->isNull() || !$item->canBePlaced()){
2201 $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z);
2203 if($hand->canBePlacedAt($blockClicked, $clickVector, $face,
true)){
2204 $blockReplace = $blockClicked;
2209 $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z);
2210 }elseif(!$hand->canBePlacedAt($blockReplace, $clickVector, $face,
false)){
2214 $tx =
new BlockTransaction($this);
2215 if(!$hand->place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player)){
2219 foreach($tx->getBlocks() as [$x, $y, $z, $block]){
2220 $block->position($this, $x, $y, $z);
2221 foreach($block->getCollisionBoxes() as $collisionBox){
2222 if(count($this->getCollidingEntities($collisionBox)) > 0){
2228 if($player !==
null){
2229 $ev =
new BlockPlaceEvent($player, $tx, $blockClicked, $item);
2230 if($player->isSpectator()){
2234 if($player->isAdventure(
true) && !$ev->isCancelled()){
2236 $itemParser = LegacyStringToItemParser::getInstance();
2238 $entry = $itemParser->parse($v);
2239 if($entry->getBlock()->hasSameTypeId($blockClicked)){
2251 if($ev->isCancelled()){
2259 foreach($tx->getBlocks() as [$x, $y, $z, $_]){
2260 $tile = $this->getTileAt($x, $y, $z);
2263 $tile->copyDataFromItem($item);
2266 $this->getBlockAt($x, $y, $z)->onPostPlace();
2270 $this->addSound($hand->getPosition(),
new BlockPlaceSound($hand));
2278 public function getEntity(
int $entityId) : ?Entity{
2279 return $this->entities[$entityId] ?? null;
2289 return $this->entities;
2305 foreach($this->getNearbyEntities($bb, $entity) as $ent){
2306 if($ent->canBeCollidedWith() && ($entity ===
null || $entity->canCollideWith($ent))){
2323 $minX = ((int) floor($bb->minX - 2)) >> Chunk::COORD_BIT_SIZE;
2324 $maxX = ((int) floor($bb->maxX + 2)) >> Chunk::COORD_BIT_SIZE;
2325 $minZ = ((int) floor($bb->minZ - 2)) >> Chunk::COORD_BIT_SIZE;
2326 $maxZ = ((int) floor($bb->maxZ + 2)) >> Chunk::COORD_BIT_SIZE;
2328 for($x = $minX; $x <= $maxX; ++$x){
2329 for($z = $minZ; $z <= $maxZ; ++$z){
2330 foreach($this->getChunkEntities($x, $z) as $ent){
2331 if($ent !== $entity && $ent->boundingBox->intersectsWith($bb)){
2353 assert(is_a($entityType,
Entity::class, true));
2355 $minX = ((int) floor($pos->x - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2356 $maxX = ((int) floor($pos->x + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2357 $minZ = ((int) floor($pos->z - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2358 $maxZ = ((int) floor($pos->z + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2360 $currentTargetDistSq = $maxDistance ** 2;
2366 $currentTarget =
null;
2368 for($x = $minX; $x <= $maxX; ++$x){
2369 for($z = $minZ; $z <= $maxZ; ++$z){
2370 foreach($this->getChunkEntities($x, $z) as $entity){
2371 if(!($entity instanceof $entityType) || $entity->isFlaggedForDespawn() || (!$includeDead && !$entity->isAlive())){
2374 $distSq = $entity->getPosition()->distanceSquared($pos);
2375 if($distSq < $currentTargetDistSq){
2376 $currentTargetDistSq = $distSq;
2377 $currentTarget = $entity;
2383 return $currentTarget;
2393 return $this->players;
2403 return $this->getTileAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z));
2410 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;
2413 public function getBiomeId(
int $x,
int $y,
int $z) : int{
2414 if(($chunk = $this->loadChunk($x >>
Chunk::COORD_BIT_SIZE, $z >>
Chunk::COORD_BIT_SIZE)) !== null){
2415 return $chunk->getBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2417 return BiomeIds::OCEAN;
2420 public function getBiome(
int $x,
int $y,
int $z) : Biome{
2421 return BiomeRegistry::getInstance()->getBiome($this->getBiomeId($x, $y, $z));
2424 public function setBiomeId(
int $x,
int $y,
int $z,
int $biomeId) : void{
2425 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
2426 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
2427 $this->unlockChunk($chunkX, $chunkZ,
null);
2428 if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !==
null){
2429 $chunk->setBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId);
2432 throw new WorldException(
"Cannot set biome in a non-generated chunk");
2441 return $this->chunks;
2444 public function getChunk(
int $chunkX,
int $chunkZ) : ?
Chunk{
2445 return $this->chunks[
World::chunkHash($chunkX, $chunkZ)] ?? null;
2453 return $this->entitiesByChunk[
World::chunkHash($chunkX, $chunkZ)] ?? [];
2460 return $this->loadChunk($pos->getFloorX() >>
Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >>
Chunk::COORD_BIT_SIZE);
2471 for($xx = -1; $xx <= 1; ++$xx){
2472 for($zz = -1; $zz <= 1; ++$zz){
2473 if($xx === 0 && $zz === 0){
2476 $result[World::chunkHash($xx, $zz)] = $this->loadChunk($x + $xx, $z + $zz);
2498 $chunkHash =
World::chunkHash($chunkX, $chunkZ);
2499 if(isset($this->chunkLock[$chunkHash])){
2500 throw new \InvalidArgumentException(
"Chunk $chunkX $chunkZ is already locked");
2502 $this->chunkLock[$chunkHash] = $lockId;
2503 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2515 $chunkHash =
World::chunkHash($chunkX, $chunkZ);
2516 if(isset($this->chunkLock[$chunkHash]) && ($lockId ===
null || $this->chunkLock[$chunkHash] === $lockId)){
2517 unset($this->chunkLock[$chunkHash]);
2518 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2530 return isset($this->chunkLock[
World::chunkHash($chunkX, $chunkZ)]);
2533 public function setChunk(
int $chunkX,
int $chunkZ,
Chunk $chunk) : void{
2534 $chunkHash =
World::chunkHash($chunkX, $chunkZ);
2535 $oldChunk = $this->loadChunk($chunkX, $chunkZ);
2536 if($oldChunk !==
null && $oldChunk !== $chunk){
2538 $transferredTiles = 0;
2539 foreach($oldChunk->getTiles() as $oldTile){
2540 $tilePosition = $oldTile->getPosition();
2541 $localX = $tilePosition->getFloorX() & Chunk::COORD_MASK;
2542 $localY = $tilePosition->getFloorY();
2543 $localZ = $tilePosition->getFloorZ() & Chunk::COORD_MASK;
2545 $newBlock = RuntimeBlockStateRegistry::getInstance()->fromStateId($chunk->
getBlockStateId($localX, $localY, $localZ));
2546 $expectedTileClass = $newBlock->getIdInfo()->getTileClass();
2548 $expectedTileClass ===
null ||
2549 !($oldTile instanceof $expectedTileClass) ||
2550 (($newTile = $chunk->
getTile($localX, $localY, $localZ)) !==
null && $newTile !== $oldTile)
2555 $transferredTiles++;
2556 $chunk->addTile($oldTile);
2557 $oldChunk->removeTile($oldTile);
2560 if($deletedTiles > 0 || $transferredTiles > 0){
2561 $this->logger->debug(
"Replacement of chunk $chunkX $chunkZ caused deletion of $deletedTiles obsolete/conflicted tiles, and transfer of $transferredTiles");
2565 $this->chunks[$chunkHash] = $chunk;
2567 unset($this->blockCache[$chunkHash]);
2568 unset($this->blockCollisionBoxCache[$chunkHash]);
2569 unset($this->changedBlocks[$chunkHash]);
2570 $chunk->setTerrainDirty();
2571 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2573 if(!$this->isChunkInUse($chunkX, $chunkZ)){
2574 $this->unloadChunkRequest($chunkX, $chunkZ);
2577 if($oldChunk ===
null){
2578 if(ChunkLoadEvent::hasHandlers()){
2579 (
new ChunkLoadEvent($this, $chunkX, $chunkZ, $chunk,
true))->call();
2582 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2583 $listener->onChunkLoaded($chunkX, $chunkZ, $chunk);
2586 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2587 $listener->onChunkChanged($chunkX, $chunkZ, $chunk);
2591 for($cX = -1; $cX <= 1; ++$cX){
2592 for($cZ = -1; $cZ <= 1; ++$cZ){
2593 foreach($this->getChunkEntities($chunkX + $cX, $chunkZ + $cZ) as $entity){
2594 $entity->onNearbyBlockChange();
2607 if(($chunk = $this->loadChunk($x >>
Chunk::COORD_BIT_SIZE, $z >>
Chunk::COORD_BIT_SIZE)) !== null){
2608 return $chunk->
getHighestBlockAt($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2610 throw new WorldException(
"Cannot get highest block in an ungenerated chunk");
2617 return $this->isChunkLoaded($pos->getFloorX() >>
Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >>
Chunk::COORD_BIT_SIZE);
2620 public function isChunkLoaded(
int $x,
int $z) : bool{
2621 return isset($this->chunks[
World::chunkHash($x, $z)]);
2624 public function isChunkGenerated(
int $x,
int $z) : bool{
2625 return $this->loadChunk($x, $z) !== null;
2628 public function isChunkPopulated(
int $x,
int $z) : bool{
2629 $chunk = $this->loadChunk($x, $z);
2630 return $chunk !==
null && $chunk->isPopulated();
2637 return
Position::fromObject($this->provider->getWorldData()->getSpawn(), $this);
2644 $previousSpawn = $this->getSpawnLocation();
2645 $this->provider->getWorldData()->setSpawn($pos);
2648 $location = Position::fromObject($pos, $this);
2649 foreach($this->players as $player){
2650 $player->getNetworkSession()->syncWorldSpawnPoint($location);
2658 if($entity->isClosed()){
2659 throw new \InvalidArgumentException(
"Attempted to add a garbage closed Entity to world");
2661 if($entity->getWorld() !== $this){
2662 throw new \InvalidArgumentException(
"Invalid Entity world");
2664 if(array_key_exists($entity->getId(), $this->entities)){
2665 if($this->entities[$entity->getId()] === $entity){
2666 throw new \InvalidArgumentException(
"Entity " . $entity->getId() .
" has already been added to this world");
2668 throw new AssumptionFailedError(
"Found two different entities sharing entity ID " . $entity->getId());
2671 $pos = $entity->getPosition()->asVector3();
2672 $this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity;
2673 $this->entityLastKnownPositions[$entity->getId()] = $pos;
2675 if($entity instanceof Player){
2676 $this->players[$entity->getId()] = $entity;
2678 $this->entities[$entity->getId()] = $entity;
2687 if($entity->getWorld() !== $this){
2688 throw new \InvalidArgumentException(
"Invalid Entity world");
2690 if(!array_key_exists($entity->getId(), $this->entities)){
2691 throw new \InvalidArgumentException(
"Entity is not tracked by this world (possibly already removed?)");
2693 $pos = $this->entityLastKnownPositions[$entity->getId()];
2694 $chunkHash = World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2695 if(isset($this->entitiesByChunk[$chunkHash][$entity->getId()])){
2696 if(count($this->entitiesByChunk[$chunkHash]) === 1){
2697 unset($this->entitiesByChunk[$chunkHash]);
2699 unset($this->entitiesByChunk[$chunkHash][$entity->getId()]);
2702 unset($this->entityLastKnownPositions[$entity->getId()]);
2704 if($entity instanceof Player){
2705 unset($this->players[$entity->getId()]);
2706 $this->checkSleep();
2709 unset($this->entities[$entity->getId()]);
2710 unset($this->updateEntities[$entity->getId()]);
2716 public function onEntityMoved(Entity $entity) : void{
2717 if(!array_key_exists($entity->getId(), $this->entityLastKnownPositions)){
2721 $oldPosition = $this->entityLastKnownPositions[$entity->getId()];
2722 $newPosition = $entity->getPosition();
2724 $oldChunkX = $oldPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2725 $oldChunkZ = $oldPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2726 $newChunkX = $newPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2727 $newChunkZ = $newPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2729 if($oldChunkX !== $newChunkX || $oldChunkZ !== $newChunkZ){
2730 $oldChunkHash = World::chunkHash($oldChunkX, $oldChunkZ);
2731 if(isset($this->entitiesByChunk[$oldChunkHash][$entity->getId()])){
2732 if(count($this->entitiesByChunk[$oldChunkHash]) === 1){
2733 unset($this->entitiesByChunk[$oldChunkHash]);
2735 unset($this->entitiesByChunk[$oldChunkHash][$entity->getId()]);
2739 $newViewers = $this->getViewersForPosition($newPosition);
2741 if(!isset($newViewers[spl_object_id($player)])){
2744 unset($newViewers[spl_object_id($player)]);
2747 foreach($newViewers as $player){
2748 $entity->spawnTo($player);
2751 $newChunkHash = World::chunkHash($newChunkX, $newChunkZ);
2752 $this->entitiesByChunk[$newChunkHash][$entity->getId()] = $entity;
2754 $this->entityLastKnownPositions[$entity->getId()] = $newPosition->asVector3();
2761 public function addTile(Tile $tile) : void{
2762 if($tile->isClosed()){
2763 throw new \InvalidArgumentException(
"Attempted to add a garbage closed Tile to world");
2765 $pos = $tile->getPosition();
2766 if(!$pos->isValid() || $pos->getWorld() !== $this){
2767 throw new \InvalidArgumentException(
"Invalid Tile world");
2769 if(!$this->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ())){
2770 throw new \InvalidArgumentException(
"Tile position is outside the world bounds");
2773 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2774 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2776 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2777 $this->chunks[$hash]->addTile($tile);
2779 throw new \InvalidArgumentException(
"Attempted to create tile " . get_class($tile) .
" in unloaded chunk $chunkX $chunkZ");
2783 $this->scheduleDelayedBlockUpdate($pos->
asVector3(), 1);
2790 public function removeTile(Tile $tile) : void{
2792 if(!$pos->isValid() || $pos->getWorld() !== $this){
2793 throw new \InvalidArgumentException(
"Invalid Tile world");
2796 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2797 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2799 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2800 $this->chunks[$hash]->removeTile($tile);
2802 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2803 $listener->onBlockChanged($pos->
asVector3());
2807 public function isChunkInUse(
int $x,
int $z) : bool{
2808 return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) && count($this->chunkLoaders[$index]) > 0;
2818 if(isset($this->chunks[$chunkHash =
World::chunkHash($x, $z)])){
2819 return $this->chunks[$chunkHash];
2822 $this->timings->syncChunkLoad->startTiming();
2824 $this->cancelUnloadChunkRequest($x, $z);
2826 $this->timings->syncChunkLoadData->startTiming();
2828 $loadedChunkData =
null;
2831 $loadedChunkData = $this->provider->loadChunk($x, $z);
2832 }
catch(CorruptedChunkException $e){
2833 $this->logger->critical(
"Failed to load chunk x=$x z=$z: " . $e->getMessage());
2836 $this->timings->syncChunkLoadData->stopTiming();
2838 if($loadedChunkData ===
null){
2839 $this->timings->syncChunkLoad->stopTiming();
2843 $chunkData = $loadedChunkData->getData();
2844 $chunk =
new Chunk($chunkData->getSubChunks(), $chunkData->isPopulated());
2845 if(!$loadedChunkData->isUpgraded()){
2846 $chunk->clearTerrainDirtyFlags();
2848 $this->logger->debug(
"Chunk $x $z has been upgraded, will be saved at the next autosave opportunity");
2850 $this->chunks[$chunkHash] = $chunk;
2851 unset($this->blockCache[$chunkHash]);
2852 unset($this->blockCollisionBoxCache[$chunkHash]);
2854 $this->initChunk($x, $z, $chunkData);
2856 if(ChunkLoadEvent::hasHandlers()){
2857 (
new ChunkLoadEvent($this, $x, $z, $this->chunks[$chunkHash],
false))->call();
2860 if(!$this->isChunkInUse($x, $z)){
2861 $this->logger->debug(
"Newly loaded chunk $x $z has no loaders registered, will be unloaded at next available opportunity");
2862 $this->unloadChunkRequest($x, $z);
2864 foreach($this->getChunkListeners($x, $z) as $listener){
2865 $listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]);
2867 $this->markTickingChunkForRecheck($x, $z);
2869 $this->timings->syncChunkLoad->stopTiming();
2871 return $this->chunks[$chunkHash];
2874 private function initChunk(
int $chunkX,
int $chunkZ, ChunkData $chunkData) : void{
2875 $logger = new \
PrefixedLogger($this->logger,
"Loading chunk $chunkX $chunkZ");
2877 if(count($chunkData->getEntityNBT()) !== 0){
2878 $this->timings->syncChunkLoadEntities->startTiming();
2879 $entityFactory = EntityFactory::getInstance();
2880 foreach($chunkData->getEntityNBT() as $k => $nbt){
2882 $entity = $entityFactory->createFromData($this, $nbt);
2883 }catch(SavedDataLoadingException $e){
2884 $logger->error(
"Bad entity data at list position $k: " . $e->getMessage());
2885 $logger->logException($e);
2888 if($entity === null){
2889 $saveIdTag = $nbt->getTag(
"identifier") ?? $nbt->getTag(
"id");
2890 $saveId =
"<unknown>";
2891 if($saveIdTag instanceof StringTag){
2892 $saveId = $saveIdTag->getValue();
2893 }elseif($saveIdTag instanceof IntTag){
2894 $saveId =
"legacy(" . $saveIdTag->getValue() .
")";
2896 $logger->warning(
"Deleted unknown entity type $saveId");
2902 $this->timings->syncChunkLoadEntities->stopTiming();
2905 if(count($chunkData->getTileNBT()) !== 0){
2906 $this->timings->syncChunkLoadTileEntities->startTiming();
2907 $tileFactory = TileFactory::getInstance();
2908 foreach($chunkData->getTileNBT() as $k => $nbt){
2910 $tile = $tileFactory->createFromData($this, $nbt);
2911 }catch(SavedDataLoadingException $e){
2912 $logger->error(
"Bad tile entity data at list position $k: " . $e->getMessage());
2913 $logger->logException($e);
2917 $logger->warning(
"Deleted unknown tile entity type " . $nbt->getString(
"id",
"<unknown>"));
2921 $tilePosition = $tile->getPosition();
2922 if(!$this->isChunkLoaded($tilePosition->getFloorX() >> Chunk::COORD_BIT_SIZE, $tilePosition->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
2923 $logger->error(
"Found tile saved on wrong chunk - unable to fix due to correct chunk not loaded");
2924 }elseif(!$this->isInWorld($tilePosition->getFloorX(), $tilePosition->getFloorY(), $tilePosition->getFloorZ())){
2925 $logger->error(
"Cannot add tile with position outside the world bounds: x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z");
2926 }elseif($this->getTile($tilePosition) !== null){
2927 $logger->error(
"Cannot add tile at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z: Another tile is already at that position");
2929 $this->addTile($tile);
2933 $this->timings->syncChunkLoadTileEntities->stopTiming();
2937 private function queueUnloadChunk(
int $x,
int $z) : void{
2938 $this->unloadQueue[World::chunkHash($x, $z)] = microtime(true);
2941 public function unloadChunkRequest(
int $x,
int $z,
bool $safe =
true) : bool{
2942 if(($safe && $this->isChunkInUse($x, $z)) || $this->isSpawnChunk($x, $z)){
2946 $this->queueUnloadChunk($x, $z);
2951 public function cancelUnloadChunkRequest(
int $x,
int $z) : void{
2952 unset($this->unloadQueue[World::chunkHash($x, $z)]);
2955 public function unloadChunk(
int $x,
int $z,
bool $safe =
true,
bool $trySave =
true) : bool{
2956 if($safe && $this->isChunkInUse($x, $z)){
2960 if(!$this->isChunkLoaded($x, $z)){
2964 $this->timings->doChunkUnload->startTiming();
2966 $chunkHash = World::chunkHash($x, $z);
2968 $chunk = $this->chunks[$chunkHash] ??
null;
2970 if($chunk !==
null){
2971 if(ChunkUnloadEvent::hasHandlers()){
2972 $ev =
new ChunkUnloadEvent($this, $x, $z, $chunk);
2974 if($ev->isCancelled()){
2975 $this->timings->doChunkUnload->stopTiming();
2981 if($trySave && $this->getAutoSave()){
2982 $this->timings->syncChunkSave->startTiming();
2984 $this->provider->saveChunk($x, $z,
new ChunkData(
2986 $chunk->isPopulated(),
2987 array_map(fn(Entity $e) => $e->saveNBT(), array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk())),
2988 array_map(fn(Tile $t) => $t->saveNBT(), $chunk->
getTiles()),
2989 ), $chunk->getTerrainDirtyFlags());
2991 $this->timings->syncChunkSave->stopTiming();
2995 foreach($this->getChunkListeners($x, $z) as $listener){
2996 $listener->onChunkUnloaded($x, $z, $chunk);
2999 foreach($this->getChunkEntities($x, $z) as $entity){
3000 if($entity instanceof Player){
3009 unset($this->chunks[$chunkHash]);
3010 unset($this->blockCache[$chunkHash]);
3011 unset($this->blockCollisionBoxCache[$chunkHash]);
3012 unset($this->changedBlocks[$chunkHash]);
3013 unset($this->registeredTickingChunks[$chunkHash]);
3014 $this->markTickingChunkForRecheck($x, $z);
3016 if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){
3017 $this->logger->debug(
"Rejecting population promise for chunk $x $z");
3018 $this->chunkPopulationRequestMap[$chunkHash]->reject();
3019 unset($this->chunkPopulationRequestMap[$chunkHash]);
3020 if(isset($this->activeChunkPopulationTasks[$chunkHash])){
3021 $this->logger->debug(
"Marking population task for chunk $x $z as orphaned");
3022 $this->activeChunkPopulationTasks[$chunkHash] =
false;
3026 $this->timings->doChunkUnload->stopTiming();
3035 $spawn = $this->getSpawnLocation();
3036 $spawnX = $spawn->x >> Chunk::COORD_BIT_SIZE;
3037 $spawnZ = $spawn->z >> Chunk::COORD_BIT_SIZE;
3039 return abs($X - $spawnX) <= 1 && abs($Z - $spawnZ) <= 1;
3051 $spawn ??= $this->getSpawnLocation();
3056 $this->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE,
null)->onCompletion(
3057 function() use ($spawn, $resolver) :
void{
3058 $spawn = $this->getSafeSpawn($spawn);
3059 $resolver->resolve($spawn);
3061 function() use ($resolver) : void{
3062 $resolver->reject();
3066 return $resolver->getPromise();
3076 if(!($spawn instanceof
Vector3) || $spawn->y <= $this->minY){
3077 $spawn = $this->getSpawnLocation();
3081 $v = $spawn->floor();
3082 $chunk = $this->getOrLoadChunkAtPosition($v);
3083 if($chunk ===
null){
3084 throw new WorldException(
"Cannot find a safe spawn point in non-generated terrain");
3088 $y = (int) min($max - 2, $v->y);
3089 $wasAir = $this->getBlockAt($x, $y - 1, $z)->getTypeId() === BlockTypeIds::AIR;
3090 for(; $y > $this->minY; --$y){
3091 if($this->getBlockAt($x, $y, $z)->isFullCube()){
3101 for(; $y >= $this->minY && $y < $max; ++$y){
3102 if(!$this->getBlockAt($x, $y + 1, $z)->isFullCube()){
3103 if(!$this->getBlockAt($x, $y, $z)->isFullCube()){
3104 return new Position($spawn->x, $y === (
int) $spawn->y ? $spawn->y : $y, $spawn->z, $this);
3111 return new Position($spawn->x, $y, $spawn->z, $this);
3125 return $this->time % self::TIME_FULL;
3133 return $this->displayName;
3142 $this->displayName = $name;
3143 $this->provider->getWorldData()->setName($name);
3150 return $this->folderName;
3157 $this->time = $time;
3165 $this->stopTime = true;
3173 $this->stopTime = false;
3181 return $this->provider->getWorldData()->getSeed();
3192 public function getDifficulty() : int{
3193 return $this->provider->getWorldData()->getDifficulty();
3196 public function setDifficulty(
int $difficulty) : void{
3197 if($difficulty < 0 || $difficulty > 3){
3198 throw new \InvalidArgumentException(
"Invalid difficulty level $difficulty");
3200 (
new WorldDifficultyChangeEvent($this, $this->getDifficulty(), $difficulty))->call();
3201 $this->provider->getWorldData()->setDifficulty($difficulty);
3203 foreach($this->players as $player){
3204 $player->getNetworkSession()->syncWorldDifficulty($this->getDifficulty());
3208 private function addChunkHashToPopulationRequestQueue(
int $chunkHash) : void{
3209 if(!isset($this->chunkPopulationRequestQueueIndex[$chunkHash])){
3210 $this->chunkPopulationRequestQueue->enqueue($chunkHash);
3211 $this->chunkPopulationRequestQueueIndex[$chunkHash] =
true;
3218 private function enqueuePopulationRequest(
int $chunkX,
int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3219 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3220 $this->addChunkHashToPopulationRequestQueue($chunkHash);
3221 $resolver = $this->chunkPopulationRequestMap[$chunkHash] =
new PromiseResolver();
3222 if($associatedChunkLoader ===
null){
3223 $temporaryLoader =
new class implements ChunkLoader{};
3224 $this->registerChunkLoader($temporaryLoader, $chunkX, $chunkZ);
3225 $resolver->getPromise()->onCompletion(
3226 fn() => $this->unregisterChunkLoader($temporaryLoader, $chunkX, $chunkZ),
3227 static function() :
void{}
3230 return $resolver->getPromise();
3233 private function drainPopulationRequestQueue() : void{
3235 while(count($this->activeChunkPopulationTasks) < $this->maxConcurrentChunkPopulationTasks && !$this->chunkPopulationRequestQueue->isEmpty()){
3236 $nextChunkHash = $this->chunkPopulationRequestQueue->dequeue();
3237 unset($this->chunkPopulationRequestQueueIndex[$nextChunkHash]);
3238 World::getXZ($nextChunkHash, $nextChunkX, $nextChunkZ);
3239 if(isset($this->chunkPopulationRequestMap[$nextChunkHash])){
3240 assert(!($this->activeChunkPopulationTasks[$nextChunkHash] ??
false),
"Population for chunk $nextChunkX $nextChunkZ already running");
3242 !$this->orderChunkPopulation($nextChunkX, $nextChunkZ,
null)->isResolved() &&
3243 !isset($this->activeChunkPopulationTasks[$nextChunkHash])
3245 $failed[] = $nextChunkHash;
3252 foreach($failed as $hash){
3253 $this->addChunkHashToPopulationRequestQueue($hash);
3262 private function checkChunkPopulationPreconditions(
int $chunkX,
int $chunkZ) : array{
3263 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3264 $resolver = $this->chunkPopulationRequestMap[$chunkHash] ??
null;
3265 if($resolver !==
null && isset($this->activeChunkPopulationTasks[$chunkHash])){
3267 return [$resolver,
false];
3270 $temporaryChunkLoader =
new class implements ChunkLoader{};
3271 $this->registerChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3272 $chunk = $this->loadChunk($chunkX, $chunkZ);
3273 $this->unregisterChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3274 if($chunk !==
null && $chunk->isPopulated()){
3276 $resolver ??=
new PromiseResolver();
3277 unset($this->chunkPopulationRequestMap[$chunkHash]);
3278 $resolver->resolve($chunk);
3279 return [$resolver,
false];
3281 return [$resolver,
true];
3296 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3297 if(!$proceedWithPopulation){
3298 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3301 if(count($this->activeChunkPopulationTasks) >= $this->maxConcurrentChunkPopulationTasks){
3303 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3305 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3319 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3320 if(!$proceedWithPopulation){
3321 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3324 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3331 private function internalOrderChunkPopulation(
int $chunkX,
int $chunkZ, ?ChunkLoader $associatedChunkLoader, ?
PromiseResolver $resolver) :
Promise{
3332 $chunkHash =
World::chunkHash($chunkX, $chunkZ);
3334 $timings = $this->timings->chunkPopulationOrder;
3335 $timings->startTiming();
3338 for($xx = -1; $xx <= 1; ++$xx){
3339 for($zz = -1; $zz <= 1; ++$zz){
3340 if($this->isChunkLocked($chunkX + $xx, $chunkZ + $zz)){
3342 return $resolver?->
getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3347 $this->activeChunkPopulationTasks[$chunkHash] =
true;
3348 if($resolver ===
null){
3349 $resolver =
new PromiseResolver();
3350 $this->chunkPopulationRequestMap[$chunkHash] = $resolver;
3353 $chunkPopulationLockId =
new ChunkLockId();
3355 $temporaryChunkLoader =
new class implements ChunkLoader{
3357 for($xx = -1; $xx <= 1; ++$xx){
3358 for($zz = -1; $zz <= 1; ++$zz){
3359 $this->lockChunk($chunkX + $xx, $chunkZ + $zz, $chunkPopulationLockId);
3360 $this->registerChunkLoader($temporaryChunkLoader, $chunkX + $xx, $chunkZ + $zz);
3364 $centerChunk = $this->loadChunk($chunkX, $chunkZ);
3365 $adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
3366 $task =
new PopulationTask(
3372 function(Chunk $centerChunk, array $adjacentChunks) use ($chunkPopulationLockId, $chunkX, $chunkZ, $temporaryChunkLoader) :
void{
3373 if(!$this->isLoaded()){
3377 $this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
3380 $workerId = $this->workerPool->selectWorker();
3381 if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
3382 $this->logger->debug(
"Selected worker $workerId previously had generator registered, but is now offline");
3383 unset($this->generatorRegisteredWorkers[$workerId]);
3385 if(!isset($this->generatorRegisteredWorkers[$workerId])){
3386 $this->registerGeneratorToWorker($workerId);
3388 $this->workerPool->submitTaskToWorker($task, $workerId);
3392 $timings->stopTiming();
3400 private function generateChunkCallback(ChunkLockId $chunkLockId,
int $x,
int $z, Chunk $chunk, array $adjacentChunks, ChunkLoader $temporaryChunkLoader) : void{
3401 $timings = $this->timings->chunkPopulationCompletion;
3402 $timings->startTiming();
3405 for($xx = -1; $xx <= 1; ++$xx){
3406 for($zz = -1; $zz <= 1; ++$zz){
3407 $this->unregisterChunkLoader($temporaryChunkLoader, $x + $xx, $z + $zz);
3408 if(!$this->unlockChunk($x + $xx, $z + $zz, $chunkLockId)){
3414 $index = World::chunkHash($x, $z);
3415 if(!isset($this->activeChunkPopulationTasks[$index])){
3416 throw new AssumptionFailedError(
"This should always be set, regardless of whether the task was orphaned or not");
3418 if(!$this->activeChunkPopulationTasks[$index]){
3419 $this->logger->debug(
"Discarding orphaned population result for chunk x=$x,z=$z");
3420 unset($this->activeChunkPopulationTasks[$index]);
3422 if($dirtyChunks === 0){
3423 $oldChunk = $this->loadChunk($x, $z);
3424 $this->setChunk($x, $z, $chunk);
3426 foreach($adjacentChunks as $relativeChunkHash => $adjacentChunk){
3427 World::getXZ($relativeChunkHash, $relativeX, $relativeZ);
3428 if($relativeX < -1 || $relativeX > 1 || $relativeZ < -1 || $relativeZ > 1){
3429 throw new AssumptionFailedError(
"Adjacent chunks should be in range -1 ... +1 coordinates");
3431 $this->setChunk($x + $relativeX, $z + $relativeZ, $adjacentChunk);
3434 if(($oldChunk ===
null || !$oldChunk->isPopulated()) && $chunk->isPopulated()){
3435 if(ChunkPopulateEvent::hasHandlers()){
3436 (
new ChunkPopulateEvent($this, $x, $z, $chunk))->call();
3439 foreach($this->getChunkListeners($x, $z) as $listener){
3440 $listener->onChunkPopulated($x, $z, $chunk);
3444 $this->logger->debug(
"Discarding population result for chunk x=$x,z=$z - terrain was modified on the main thread before async population completed");
3453 unset($this->activeChunkPopulationTasks[$index]);
3455 if($dirtyChunks === 0){
3456 $promise = $this->chunkPopulationRequestMap[$index] ??
null;
3457 if($promise !==
null){
3458 unset($this->chunkPopulationRequestMap[$index]);
3459 $promise->resolve($chunk);
3462 $this->logger->debug(
"Unable to resolve population promise for chunk x=$x,z=$z - populated chunk was forcibly unloaded while setting modified chunks");
3468 $this->addChunkHashToPopulationRequestQueue($index);
3471 $this->drainPopulationRequestQueue();
3473 $timings->stopTiming();
3476 public function doChunkGarbageCollection() : void{
3477 $this->timings->doChunkGC->startTiming();
3479 foreach($this->chunks as $index => $chunk){
3480 if(!isset($this->unloadQueue[$index])){
3481 World::getXZ($index, $X, $Z);
3482 if(!$this->isSpawnChunk($X, $Z)){
3483 $this->unloadChunkRequest($X, $Z,
true);
3486 $chunk->collectGarbage();
3489 $this->provider->doGarbageCollection();
3491 $this->timings->doChunkGC->stopTiming();
3494 public function unloadChunks(
bool $force =
false) : void{
3495 if(count($this->unloadQueue) > 0){
3497 $now = microtime(
true);
3498 foreach($this->unloadQueue as $index => $time){
3499 World::getXZ($index, $X, $Z);
3502 if($maxUnload <= 0){
3504 }elseif($time > ($now - 30)){
3510 if($this->unloadChunk($X, $Z,
true)){
3511 unset($this->unloadQueue[$index]);