PocketMine-MP 5.28.3 git-d5a1007c80fcee27feb2251cf5dcf1ad5a59a85c
Loading...
Searching...
No Matches
World.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
27namespace pocketmine\world;
28
61use pocketmine\item\ItemUseResult;
94use pocketmine\world\format\LightArray;
111use function abs;
112use function array_filter;
113use function array_key_exists;
114use function array_keys;
115use function array_map;
116use function array_merge;
117use function array_sum;
118use function array_values;
119use function assert;
120use function cos;
121use function count;
122use function floor;
123use function get_class;
124use function gettype;
125use function is_a;
126use function is_object;
127use function max;
128use function microtime;
129use function min;
130use function morton2d_decode;
131use function morton2d_encode;
132use function morton3d_decode;
133use function morton3d_encode;
134use function mt_rand;
135use function preg_match;
136use function spl_object_id;
137use function strtolower;
138use function trim;
139use const M_PI;
140use const PHP_INT_MAX;
141use const PHP_INT_MIN;
142
143#include <rules/World.h>
144
150class World implements ChunkManager{
151
152 private static int $worldIdCounter = 1;
153
154 public const Y_MAX = 320;
155 public const Y_MIN = -64;
156
157 public const TIME_DAY = 1000;
158 public const TIME_NOON = 6000;
159 public const TIME_SUNSET = 12000;
160 public const TIME_NIGHT = 13000;
161 public const TIME_MIDNIGHT = 18000;
162 public const TIME_SUNRISE = 23000;
163
164 public const TIME_FULL = 24000;
165
166 public const DIFFICULTY_PEACEFUL = 0;
167 public const DIFFICULTY_EASY = 1;
168 public const DIFFICULTY_NORMAL = 2;
169 public const DIFFICULTY_HARD = 3;
170
171 public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3;
172
173 //TODO: this could probably do with being a lot bigger
174 private const BLOCK_CACHE_SIZE_CAP = 2048;
175
180 private array $players = [];
181
186 private array $entities = [];
191 private array $entityLastKnownPositions = [];
192
197 private array $entitiesByChunk = [];
198
203 public array $updateEntities = [];
204
205 private bool $inDynamicStateRecalculation = false;
210 private array $blockCache = [];
211 private int $blockCacheSize = 0;
216 private array $blockCollisionBoxCache = [];
217
218 private int $sendTimeTicker = 0;
219
220 private int $worldId;
221
222 private int $providerGarbageCollectionTicker = 0;
223
224 private int $minY;
225 private int $maxY;
226
231 private array $registeredTickingChunks = [];
232
239 private array $validTickingChunks = [];
240
246 private array $recheckTickingChunks = [];
247
252 private array $chunkLoaders = [];
253
258 private array $chunkListeners = [];
263 private array $playerChunkListeners = [];
264
269 private array $packetBuffersByChunk = [];
270
275 private array $unloadQueue = [];
276
277 private int $time;
278 public bool $stopTime = false;
279
280 private float $sunAnglePercentage = 0.0;
281 private int $skyLightReduction = 0;
282
283 private string $folderName;
284 private string $displayName;
285
290 private array $chunks = [];
291
296 private array $changedBlocks = [];
297
299 private ReversePriorityQueue $scheduledBlockUpdateQueue;
304 private array $scheduledBlockUpdateQueueIndex = [];
305
307 private \SplQueue $neighbourBlockUpdateQueue;
312 private array $neighbourBlockUpdateQueueIndex = [];
313
318 private array $activeChunkPopulationTasks = [];
323 private array $chunkLock = [];
324 private int $maxConcurrentChunkPopulationTasks = 2;
329 private array $chunkPopulationRequestMap = [];
334 private \SplQueue $chunkPopulationRequestQueue;
339 private array $chunkPopulationRequestQueueIndex = [];
340
341 private readonly GeneratorExecutor $generatorExecutor;
342
343 private bool $autoSave = true;
344
345 private int $sleepTicks = 0;
346
347 private int $chunkTickRadius;
348 private int $tickedBlocksPerSubchunkPerTick = self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK;
353 private array $randomTickBlocks = [];
354
355 public WorldTimings $timings;
356
357 public float $tickRateTime = 0;
358
359 private bool $doingTick = false;
360
361 private bool $unloaded = false;
366 private array $unloadCallbacks = [];
367
368 private ?BlockLightUpdate $blockLightUpdate = null;
369 private ?SkyLightUpdate $skyLightUpdate = null;
370
371 private \Logger $logger;
372
373 private RuntimeBlockStateRegistry $blockStateRegistry;
374
378 public static function chunkHash(int $x, int $z) : int{
379 return morton2d_encode($x, $z);
380 }
381
382 private const MORTON3D_BIT_SIZE = 21;
383 private const BLOCKHASH_Y_BITS = 9;
384 private const BLOCKHASH_Y_PADDING = 64; //size (in blocks) of padding after both boundaries of the Y axis
385 private const BLOCKHASH_Y_OFFSET = self::BLOCKHASH_Y_PADDING - self::Y_MIN;
386 private const BLOCKHASH_Y_MASK = (1 << self::BLOCKHASH_Y_BITS) - 1;
387 private const BLOCKHASH_XZ_MASK = (1 << self::MORTON3D_BIT_SIZE) - 1;
388 private const BLOCKHASH_XZ_EXTRA_BITS = (self::MORTON3D_BIT_SIZE - self::BLOCKHASH_Y_BITS) >> 1;
389 private const BLOCKHASH_XZ_EXTRA_MASK = (1 << self::BLOCKHASH_XZ_EXTRA_BITS) - 1;
390 private const BLOCKHASH_XZ_SIGN_SHIFT = 64 - self::MORTON3D_BIT_SIZE - self::BLOCKHASH_XZ_EXTRA_BITS;
391 private const BLOCKHASH_X_SHIFT = self::BLOCKHASH_Y_BITS;
392 private const BLOCKHASH_Z_SHIFT = self::BLOCKHASH_X_SHIFT + self::BLOCKHASH_XZ_EXTRA_BITS;
393
397 public static function blockHash(int $x, int $y, int $z) : int{
398 $shiftedY = $y + self::BLOCKHASH_Y_OFFSET;
399 if(($shiftedY & (~0 << self::BLOCKHASH_Y_BITS)) !== 0){
400 throw new \InvalidArgumentException("Y coordinate $y is out of range!");
401 }
402 //morton3d gives us 21 bits on each axis, but the Y axis only requires 9
403 //so we use the extra space on Y (12 bits) and add 6 extra bits from X and Z instead.
404 //if we ever need more space for Y (e.g. due to expansion), take bits from X/Z to compensate.
405 return morton3d_encode(
406 $x & self::BLOCKHASH_XZ_MASK,
407 ($shiftedY /* & self::BLOCKHASH_Y_MASK */) |
408 ((($x >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_X_SHIFT) |
409 ((($z >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_Z_SHIFT),
410 $z & self::BLOCKHASH_XZ_MASK
411 );
412 }
413
417 public static function chunkBlockHash(int $x, int $y, int $z) : int{
418 return morton3d_encode($x, $y, $z);
419 }
420
427 public static function getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z) : void{
428 [$baseX, $baseY, $baseZ] = morton3d_decode($hash);
429
430 $extraX = ((($baseY >> self::BLOCKHASH_X_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
431 $extraZ = ((($baseY >> self::BLOCKHASH_Z_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
432
433 $x = (($baseX & self::BLOCKHASH_XZ_MASK) | $extraX) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
434 $y = ($baseY & self::BLOCKHASH_Y_MASK) - self::BLOCKHASH_Y_OFFSET;
435 $z = (($baseZ & self::BLOCKHASH_XZ_MASK) | $extraZ) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
436 }
437
443 public static function getXZ(int $hash, ?int &$x, ?int &$z) : void{
444 [$x, $z] = morton2d_decode($hash);
445 }
446
447 public static function getDifficultyFromString(string $str) : int{
448 switch(strtolower(trim($str))){
449 case "0":
450 case "peaceful":
451 case "p":
452 return World::DIFFICULTY_PEACEFUL;
453
454 case "1":
455 case "easy":
456 case "e":
457 return World::DIFFICULTY_EASY;
458
459 case "2":
460 case "normal":
461 case "n":
462 return World::DIFFICULTY_NORMAL;
463
464 case "3":
465 case "hard":
466 case "h":
467 return World::DIFFICULTY_HARD;
468 }
469
470 return -1;
471 }
472
476 public function __construct(
477 private Server $server,
478 string $name, //TODO: this should be folderName (named arguments BC break)
479 private WritableWorldProvider $provider,
480 private AsyncPool $workerPool
481 ){
482 $this->folderName = $name;
483 $this->worldId = self::$worldIdCounter++;
484
485 $this->displayName = $this->provider->getWorldData()->getName();
486 $this->logger = new \PrefixedLogger($server->getLogger(), "World: $this->displayName");
487
488 $this->blockStateRegistry = RuntimeBlockStateRegistry::getInstance();
489 $this->minY = $this->provider->getWorldMinY();
490 $this->maxY = $this->provider->getWorldMaxY();
491
492 $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_preparing($this->displayName)));
493 $generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
494 throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
495 $generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
496
497 $executorSetupParameters = new GeneratorExecutorSetupParameters(
498 worldMinY: $this->minY,
499 worldMaxY: $this->maxY,
500 generatorSeed: $this->getSeed(),
501 generatorClass: $generator->getGeneratorClass(),
502 generatorSettings: $this->provider->getWorldData()->getGeneratorOptions()
503 );
504 $this->generatorExecutor = $generator->isFast() ?
505 new SyncGeneratorExecutor($executorSetupParameters) :
507 $this->logger,
508 $this->workerPool,
509 $executorSetupParameters,
510 $this->worldId
511 );
512
513 $this->chunkPopulationRequestQueue = new \SplQueue();
514 $this->addOnUnloadCallback(function() : void{
515 $this->logger->debug("Cancelling unfulfilled generation requests");
516
517 foreach($this->chunkPopulationRequestMap as $chunkHash => $promise){
518 $promise->reject();
519 unset($this->chunkPopulationRequestMap[$chunkHash]);
520 }
521 if(count($this->chunkPopulationRequestMap) !== 0){
522 //TODO: this might actually get hit because generation rejection callbacks might try to schedule new
523 //requests, and we can't prevent that right now because there's no way to detect "unloading" state
524 throw new AssumptionFailedError("New generation requests scheduled during unload");
525 }
526 });
527
528 $this->scheduledBlockUpdateQueue = new ReversePriorityQueue();
529 $this->scheduledBlockUpdateQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
530
531 $this->neighbourBlockUpdateQueue = new \SplQueue();
532
533 $this->time = $this->provider->getWorldData()->getTime();
534
535 $cfg = $this->server->getConfigGroup();
536 $this->chunkTickRadius = min($this->server->getViewDistance(), max(0, $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_TICK_RADIUS, 4)));
537 if($cfg->getPropertyInt("chunk-ticking.per-tick", 40) <= 0){
538 //TODO: this needs l10n
539 $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.");
540 $this->chunkTickRadius = 0;
541 }
542 $this->tickedBlocksPerSubchunkPerTick = $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_BLOCKS_PER_SUBCHUNK_PER_TICK, self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK);
543 $this->maxConcurrentChunkPopulationTasks = $cfg->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2);
544
545 $this->initRandomTickBlocksFromConfig($cfg);
546
547 $this->timings = new WorldTimings($this);
548 }
549
550 private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{
551 $dontTickBlocks = [];
552 $parser = StringToItemParser::getInstance();
553 foreach($cfg->getProperty(YmlServerProperties::CHUNK_TICKING_DISABLE_BLOCK_TICKING, []) as $name){
554 $name = (string) $name;
555 $item = $parser->parse($name);
556 if($item !== null){
557 $block = $item->getBlock();
558 }elseif(preg_match("/^-?\d+$/", $name) === 1){
559 //TODO: this is a really sketchy hack - remove this as soon as possible
560 try{
561 $blockStateData = GlobalBlockStateHandlers::getUpgrader()->upgradeIntIdMeta((int) $name, 0);
562 }catch(BlockStateDeserializeException){
563 continue;
564 }
565 $block = $this->blockStateRegistry->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData));
566 }else{
567 //TODO: we probably ought to log an error here
568 continue;
569 }
570
571 if($block->getTypeId() !== BlockTypeIds::AIR){
572 $dontTickBlocks[$block->getTypeId()] = $name;
573 }
574 }
575
576 foreach($this->blockStateRegistry->getAllKnownStates() as $state){
577 $dontTickName = $dontTickBlocks[$state->getTypeId()] ?? null;
578 if($dontTickName === null && $state->ticksRandomly()){
579 $this->randomTickBlocks[$state->getStateId()] = true;
580 }
581 }
582 }
583
584 public function getTickRateTime() : float{
585 return $this->tickRateTime;
586 }
587
588 public function getServer() : Server{
589 return $this->server;
590 }
591
592 public function getLogger() : \Logger{
593 return $this->logger;
594 }
595
596 final public function getProvider() : WritableWorldProvider{
597 return $this->provider;
598 }
599
603 final public function getId() : int{
604 return $this->worldId;
605 }
606
607 public function isLoaded() : bool{
608 return !$this->unloaded;
609 }
610
614 public function onUnload() : void{
615 if($this->unloaded){
616 throw new \LogicException("Tried to close a world which is already closed");
617 }
618
619 foreach($this->unloadCallbacks as $callback){
620 $callback();
621 }
622 $this->unloadCallbacks = [];
623
624 foreach($this->chunks as $chunkHash => $chunk){
625 self::getXZ($chunkHash, $chunkX, $chunkZ);
626 $this->unloadChunk($chunkX, $chunkZ, false);
627 }
628 foreach($this->entitiesByChunk as $chunkHash => $entities){
629 self::getXZ($chunkHash, $chunkX, $chunkZ);
630
631 $leakedEntities = 0;
632 foreach($entities as $entity){
633 if(!$entity->isFlaggedForDespawn()){
634 $leakedEntities++;
635 }
636 $entity->close();
637 }
638 if($leakedEntities !== 0){
639 $this->logger->warning("$leakedEntities leaked entities found in ungenerated chunk $chunkX $chunkZ during unload, they won't be saved!");
640 }
641 }
642
643 $this->save();
644
645 $this->generatorExecutor->shutdown();
646
647 $this->provider->close();
648 $this->blockCache = [];
649 $this->blockCacheSize = 0;
650 $this->blockCollisionBoxCache = [];
651
652 $this->unloaded = true;
653 }
654
656 public function addOnUnloadCallback(\Closure $callback) : void{
657 $this->unloadCallbacks[spl_object_id($callback)] = $callback;
658 }
659
661 public function removeOnUnloadCallback(\Closure $callback) : void{
662 unset($this->unloadCallbacks[spl_object_id($callback)]);
663 }
664
673 private function filterViewersForPosition(Vector3 $pos, array $allowed) : array{
674 $candidates = $this->getViewersForPosition($pos);
675 $filtered = [];
676 foreach($allowed as $player){
677 $k = spl_object_id($player);
678 if(isset($candidates[$k])){
679 $filtered[$k] = $candidates[$k];
680 }
681 }
682
683 return $filtered;
684 }
685
689 public function addSound(Vector3 $pos, Sound $sound, ?array $players = null) : void{
690 $players ??= $this->getViewersForPosition($pos);
691
692 if(WorldSoundEvent::hasHandlers()){
693 $ev = new WorldSoundEvent($this, $sound, $pos, $players);
694 $ev->call();
695 if($ev->isCancelled()){
696 return;
697 }
698
699 $sound = $ev->getSound();
700 $players = $ev->getRecipients();
701 }
702
703 $pk = $sound->encode($pos);
704 if(count($pk) > 0){
705 if($players === $this->getViewersForPosition($pos)){
706 foreach($pk as $e){
707 $this->broadcastPacketToViewers($pos, $e);
708 }
709 }else{
710 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
711 }
712 }
713 }
714
718 public function addParticle(Vector3 $pos, Particle $particle, ?array $players = null) : void{
719 $players ??= $this->getViewersForPosition($pos);
720
721 if(WorldParticleEvent::hasHandlers()){
722 $ev = new WorldParticleEvent($this, $particle, $pos, $players);
723 $ev->call();
724 if($ev->isCancelled()){
725 return;
726 }
727
728 $particle = $ev->getParticle();
729 $players = $ev->getRecipients();
730 }
731
732 $pk = $particle->encode($pos);
733 if(count($pk) > 0){
734 if($players === $this->getViewersForPosition($pos)){
735 foreach($pk as $e){
736 $this->broadcastPacketToViewers($pos, $e);
737 }
738 }else{
739 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
740 }
741 }
742 }
743
744 public function getAutoSave() : bool{
745 return $this->autoSave;
746 }
747
748 public function setAutoSave(bool $value) : void{
749 $this->autoSave = $value;
750 }
751
761 public function getChunkPlayers(int $chunkX, int $chunkZ) : array{
762 return $this->playerChunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? [];
763 }
764
771 public function getChunkLoaders(int $chunkX, int $chunkZ) : array{
772 return $this->chunkLoaders[World::chunkHash($chunkX, $chunkZ)] ?? [];
773 }
774
781 public function getViewersForPosition(Vector3 $pos) : array{
782 return $this->getChunkPlayers($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
783 }
784
788 public function broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet) : void{
789 $this->broadcastPacketToPlayersUsingChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE, $packet);
790 }
791
792 private function broadcastPacketToPlayersUsingChunk(int $chunkX, int $chunkZ, ClientboundPacket $packet) : void{
793 if(!isset($this->packetBuffersByChunk[$index = World::chunkHash($chunkX, $chunkZ)])){
794 $this->packetBuffersByChunk[$index] = [$packet];
795 }else{
796 $this->packetBuffersByChunk[$index][] = $packet;
797 }
798 }
799
800 public function registerChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ, bool $autoLoad = true) : void{
801 $loaderId = spl_object_id($loader);
802
803 if(!isset($this->chunkLoaders[$chunkHash = World::chunkHash($chunkX, $chunkZ)])){
804 $this->chunkLoaders[$chunkHash] = [];
805 }elseif(isset($this->chunkLoaders[$chunkHash][$loaderId])){
806 return;
807 }
808
809 $this->chunkLoaders[$chunkHash][$loaderId] = $loader;
810
811 $this->cancelUnloadChunkRequest($chunkX, $chunkZ);
812
813 if($autoLoad){
814 $this->loadChunk($chunkX, $chunkZ);
815 }
816 }
817
818 public function unregisterChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ) : void{
819 $chunkHash = World::chunkHash($chunkX, $chunkZ);
820 $loaderId = spl_object_id($loader);
821 if(isset($this->chunkLoaders[$chunkHash][$loaderId])){
822 if(count($this->chunkLoaders[$chunkHash]) === 1){
823 unset($this->chunkLoaders[$chunkHash]);
824 $this->unloadChunkRequest($chunkX, $chunkZ, true);
825 if(isset($this->chunkPopulationRequestMap[$chunkHash]) && !isset($this->activeChunkPopulationTasks[$chunkHash])){
826 $this->chunkPopulationRequestMap[$chunkHash]->reject();
827 unset($this->chunkPopulationRequestMap[$chunkHash]);
828 }
829 }else{
830 unset($this->chunkLoaders[$chunkHash][$loaderId]);
831 }
832 }
833 }
834
838 public function registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{
839 $hash = World::chunkHash($chunkX, $chunkZ);
840 if(isset($this->chunkListeners[$hash])){
841 $this->chunkListeners[$hash][spl_object_id($listener)] = $listener;
842 }else{
843 $this->chunkListeners[$hash] = [spl_object_id($listener) => $listener];
844 }
845 if($listener instanceof Player){
846 $this->playerChunkListeners[$hash][spl_object_id($listener)] = $listener;
847 }
848 }
849
855 public function unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{
856 $hash = World::chunkHash($chunkX, $chunkZ);
857 if(isset($this->chunkListeners[$hash])){
858 if(count($this->chunkListeners[$hash]) === 1){
859 unset($this->chunkListeners[$hash]);
860 unset($this->playerChunkListeners[$hash]);
861 }else{
862 unset($this->chunkListeners[$hash][spl_object_id($listener)]);
863 unset($this->playerChunkListeners[$hash][spl_object_id($listener)]);
864 }
865 }
866 }
867
871 public function unregisterChunkListenerFromAll(ChunkListener $listener) : void{
872 foreach($this->chunkListeners as $hash => $listeners){
873 World::getXZ($hash, $chunkX, $chunkZ);
874 $this->unregisterChunkListener($listener, $chunkX, $chunkZ);
875 }
876 }
877
884 public function getChunkListeners(int $chunkX, int $chunkZ) : array{
885 return $this->chunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? [];
886 }
887
891 public function sendTime(Player ...$targets) : void{
892 if(count($targets) === 0){
893 $targets = $this->players;
894 }
895 foreach($targets as $player){
896 $player->getNetworkSession()->syncWorldTime($this->time);
897 }
898 }
899
900 public function isDoingTick() : bool{
901 return $this->doingTick;
902 }
903
907 public function doTick(int $currentTick) : void{
908 if($this->unloaded){
909 throw new \LogicException("Attempted to tick a world which has been closed");
910 }
911
912 $this->timings->doTick->startTiming();
913 $this->doingTick = true;
914 try{
915 $this->actuallyDoTick($currentTick);
916 }finally{
917 $this->doingTick = false;
918 $this->timings->doTick->stopTiming();
919 }
920 }
921
922 protected function actuallyDoTick(int $currentTick) : void{
923 if(!$this->stopTime){
924 //this simulates an overflow, as would happen in any language which doesn't do stupid things to var types
925 if($this->time === PHP_INT_MAX){
926 $this->time = PHP_INT_MIN;
927 }else{
928 $this->time++;
929 }
930 }
931
932 $this->sunAnglePercentage = $this->computeSunAnglePercentage(); //Sun angle depends on the current time
933 $this->skyLightReduction = $this->computeSkyLightReduction(); //Sky light reduction depends on the sun angle
934
935 if(++$this->sendTimeTicker === 200){
936 $this->sendTime();
937 $this->sendTimeTicker = 0;
938 }
939
940 $this->unloadChunks();
941 if(++$this->providerGarbageCollectionTicker >= 6000){
942 $this->provider->doGarbageCollection();
943 $this->providerGarbageCollectionTicker = 0;
944 }
945
946 $this->timings->scheduledBlockUpdates->startTiming();
947 //Delayed updates
948 while($this->scheduledBlockUpdateQueue->count() > 0 && $this->scheduledBlockUpdateQueue->current()["priority"] <= $currentTick){
950 $vec = $this->scheduledBlockUpdateQueue->extract()["data"];
951 unset($this->scheduledBlockUpdateQueueIndex[World::blockHash($vec->x, $vec->y, $vec->z)]);
952 if(!$this->isInLoadedTerrain($vec)){
953 continue;
954 }
955 $block = $this->getBlock($vec);
956 $block->onScheduledUpdate();
957 }
958 $this->timings->scheduledBlockUpdates->stopTiming();
959
960 $this->timings->neighbourBlockUpdates->startTiming();
961 //Normal updates
962 while($this->neighbourBlockUpdateQueue->count() > 0){
963 $index = $this->neighbourBlockUpdateQueue->dequeue();
964 unset($this->neighbourBlockUpdateQueueIndex[$index]);
965 World::getBlockXYZ($index, $x, $y, $z);
966 if(!$this->isChunkLoaded($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)){
967 continue;
968 }
969
970 $block = $this->getBlockAt($x, $y, $z);
971
972 if(BlockUpdateEvent::hasHandlers()){
973 $ev = new BlockUpdateEvent($block);
974 $ev->call();
975 if($ev->isCancelled()){
976 continue;
977 }
978 }
979 foreach($this->getNearbyEntities(AxisAlignedBB::one()->offset($x, $y, $z)) as $entity){
980 $entity->onNearbyBlockChange();
981 }
982 $block->onNearbyBlockChange();
983 }
984
985 $this->timings->neighbourBlockUpdates->stopTiming();
986
987 $this->timings->entityTick->startTiming();
988 //Update entities that need update
989 foreach($this->updateEntities as $id => $entity){
990 if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->onUpdate($currentTick)){
991 unset($this->updateEntities[$id]);
992 }
993 if($entity->isFlaggedForDespawn()){
994 $entity->close();
995 }
996 }
997 $this->timings->entityTick->stopTiming();
998
999 $this->timings->randomChunkUpdates->startTiming();
1000 $this->tickChunks();
1001 $this->timings->randomChunkUpdates->stopTiming();
1002
1003 $this->executeQueuedLightUpdates();
1004
1005 if(count($this->changedBlocks) > 0){
1006 if(count($this->players) > 0){
1007 foreach($this->changedBlocks as $index => $blocks){
1008 if(count($blocks) === 0){ //blocks can be set normally and then later re-set with direct send
1009 continue;
1010 }
1011 World::getXZ($index, $chunkX, $chunkZ);
1012 if(!$this->isChunkLoaded($chunkX, $chunkZ)){
1013 //a previous chunk may have caused this one to be unloaded by a ChunkListener
1014 continue;
1015 }
1016 if(count($blocks) > 512){
1017 $chunk = $this->getChunk($chunkX, $chunkZ) ?? throw new AssumptionFailedError("We already checked that the chunk is loaded");
1018 foreach($this->getChunkPlayers($chunkX, $chunkZ) as $p){
1019 $p->onChunkChanged($chunkX, $chunkZ, $chunk);
1020 }
1021 }else{
1022 foreach($this->createBlockUpdatePackets($blocks) as $packet){
1023 $this->broadcastPacketToPlayersUsingChunk($chunkX, $chunkZ, $packet);
1024 }
1025 }
1026 }
1027 }
1028
1029 $this->changedBlocks = [];
1030
1031 }
1032
1033 if($this->sleepTicks > 0 && --$this->sleepTicks <= 0){
1034 $this->checkSleep();
1035 }
1036
1037 foreach($this->packetBuffersByChunk as $index => $entries){
1038 World::getXZ($index, $chunkX, $chunkZ);
1039 $chunkPlayers = $this->getChunkPlayers($chunkX, $chunkZ);
1040 if(count($chunkPlayers) > 0){
1041 NetworkBroadcastUtils::broadcastPackets($chunkPlayers, $entries);
1042 }
1043 }
1044
1045 $this->packetBuffersByChunk = [];
1046 }
1047
1048 public function checkSleep() : void{
1049 if(count($this->players) === 0){
1050 return;
1051 }
1052
1053 $resetTime = true;
1054 foreach($this->getPlayers() as $p){
1055 if(!$p->isSleeping()){
1056 $resetTime = false;
1057 break;
1058 }
1059 }
1060
1061 if($resetTime){
1062 $time = $this->getTimeOfDay();
1063
1064 if($time >= World::TIME_NIGHT && $time < World::TIME_SUNRISE){
1065 $this->setTime($this->getTime() + World::TIME_FULL - $time);
1066
1067 foreach($this->getPlayers() as $p){
1068 $p->stopSleep();
1069 }
1070 }
1071 }
1072 }
1073
1074 public function setSleepTicks(int $ticks) : void{
1075 $this->sleepTicks = $ticks;
1076 }
1077
1084 public function createBlockUpdatePackets(array $blocks) : array{
1085 $packets = [];
1086
1087 $blockTranslator = TypeConverter::getInstance()->getBlockTranslator();
1088
1089 foreach($blocks as $b){
1090 if(!($b instanceof Vector3)){
1091 throw new \TypeError("Expected Vector3 in blocks array, got " . (is_object($b) ? get_class($b) : gettype($b)));
1092 }
1093
1094 $fullBlock = $this->getBlockAt($b->x, $b->y, $b->z);
1095 $blockPosition = BlockPosition::fromVector3($b);
1096
1097 $tile = $this->getTileAt($b->x, $b->y, $b->z);
1098 if($tile instanceof Spawnable){
1099 $expectedClass = $fullBlock->getIdInfo()->getTileClass();
1100 if($expectedClass !== null && $tile instanceof $expectedClass && count($fakeStateProperties = $tile->getRenderUpdateBugWorkaroundStateProperties($fullBlock)) > 0){
1101 $originalStateData = $blockTranslator->internalIdToNetworkStateData($fullBlock->getStateId());
1102 $fakeStateData = new BlockStateData(
1103 $originalStateData->getName(),
1104 array_merge($originalStateData->getStates(), $fakeStateProperties),
1105 $originalStateData->getVersion()
1106 );
1107 $packets[] = UpdateBlockPacket::create(
1108 $blockPosition,
1109 $blockTranslator->getBlockStateDictionary()->lookupStateIdFromData($fakeStateData) ?? throw new AssumptionFailedError("Unmapped fake blockstate data: " . $fakeStateData->toNbt()),
1110 UpdateBlockPacket::FLAG_NETWORK,
1111 UpdateBlockPacket::DATA_LAYER_NORMAL
1112 );
1113 }
1114 }
1115 $packets[] = UpdateBlockPacket::create(
1116 $blockPosition,
1117 $blockTranslator->internalIdToNetworkId($fullBlock->getStateId()),
1118 UpdateBlockPacket::FLAG_NETWORK,
1119 UpdateBlockPacket::DATA_LAYER_NORMAL
1120 );
1121
1122 if($tile instanceof Spawnable){
1123 $packets[] = BlockActorDataPacket::create($blockPosition, $tile->getSerializedSpawnCompound());
1124 }
1125 }
1126
1127 return $packets;
1128 }
1129
1130 public function clearCache(bool $force = false) : void{
1131 if($force){
1132 $this->blockCache = [];
1133 $this->blockCacheSize = 0;
1134 $this->blockCollisionBoxCache = [];
1135 }else{
1136 //Recalculate this when we're asked - blockCacheSize may be higher than the real size
1137 $this->blockCacheSize = 0;
1138 foreach($this->blockCache as $list){
1139 $this->blockCacheSize += count($list);
1140 if($this->blockCacheSize > self::BLOCK_CACHE_SIZE_CAP){
1141 $this->blockCache = [];
1142 $this->blockCacheSize = 0;
1143 break;
1144 }
1145 }
1146
1147 $count = 0;
1148 foreach($this->blockCollisionBoxCache as $list){
1149 $count += count($list);
1150 if($count > self::BLOCK_CACHE_SIZE_CAP){
1151 //TODO: Is this really the best logic?
1152 $this->blockCollisionBoxCache = [];
1153 break;
1154 }
1155 }
1156 }
1157 }
1158
1159 private function trimBlockCache() : void{
1160 $before = $this->blockCacheSize;
1161 //Since PHP maintains key order, earliest in foreach should be the oldest entries
1162 //Older entries are less likely to be hot, so destroying these should usually have the lowest impact on performance
1163 foreach($this->blockCache as $chunkHash => $blocks){
1164 unset($this->blockCache[$chunkHash]);
1165 $this->blockCacheSize -= count($blocks);
1166 if($this->blockCacheSize < self::BLOCK_CACHE_SIZE_CAP){
1167 break;
1168 }
1169 }
1170 }
1171
1176 public function getRandomTickedBlocks() : array{
1177 return $this->randomTickBlocks;
1178 }
1179
1180 public function addRandomTickedBlock(Block $block) : void{
1181 if($block instanceof UnknownBlock){
1182 throw new \InvalidArgumentException("Cannot do random-tick on unknown block");
1183 }
1184 $this->randomTickBlocks[$block->getStateId()] = true;
1185 }
1186
1187 public function removeRandomTickedBlock(Block $block) : void{
1188 unset($this->randomTickBlocks[$block->getStateId()]);
1189 }
1190
1195 public function getChunkTickRadius() : int{
1196 return $this->chunkTickRadius;
1197 }
1198
1203 public function setChunkTickRadius(int $radius) : void{
1204 $this->chunkTickRadius = $radius;
1205 }
1206
1214 public function getTickingChunks() : array{
1215 return array_keys($this->validTickingChunks);
1216 }
1217
1222 public function registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
1223 $chunkPosHash = World::chunkHash($chunkX, $chunkZ);
1224 $this->registeredTickingChunks[$chunkPosHash][spl_object_id($ticker)] = $ticker;
1225 $this->recheckTickingChunks[$chunkPosHash] = $chunkPosHash;
1226 }
1227
1232 public function unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
1233 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1234 $tickerId = spl_object_id($ticker);
1235 if(isset($this->registeredTickingChunks[$chunkHash][$tickerId])){
1236 if(count($this->registeredTickingChunks[$chunkHash]) === 1){
1237 unset(
1238 $this->registeredTickingChunks[$chunkHash],
1239 $this->recheckTickingChunks[$chunkHash],
1240 $this->validTickingChunks[$chunkHash]
1241 );
1242 }else{
1243 unset($this->registeredTickingChunks[$chunkHash][$tickerId]);
1244 }
1245 }
1246 }
1247
1248 private function tickChunks() : void{
1249 if($this->chunkTickRadius <= 0 || count($this->registeredTickingChunks) === 0){
1250 return;
1251 }
1252
1253 if(count($this->recheckTickingChunks) > 0){
1254 $this->timings->randomChunkUpdatesChunkSelection->startTiming();
1255
1256 $chunkTickableCache = [];
1257
1258 foreach($this->recheckTickingChunks as $hash => $_){
1259 World::getXZ($hash, $chunkX, $chunkZ);
1260 if($this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){
1261 $this->validTickingChunks[$hash] = $hash;
1262 }
1263 }
1264 $this->recheckTickingChunks = [];
1265
1266 $this->timings->randomChunkUpdatesChunkSelection->stopTiming();
1267 }
1268
1269 foreach($this->validTickingChunks as $index => $_){
1270 World::getXZ($index, $chunkX, $chunkZ);
1271
1272 $this->tickChunk($chunkX, $chunkZ);
1273 }
1274 }
1275
1282 private function isChunkTickable(int $chunkX, int $chunkZ, array &$cache) : bool{
1283 for($cx = -1; $cx <= 1; ++$cx){
1284 for($cz = -1; $cz <= 1; ++$cz){
1285 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1286 if(isset($cache[$chunkHash])){
1287 if(!$cache[$chunkHash]){
1288 return false;
1289 }
1290 continue;
1291 }
1292 if($this->isChunkLocked($chunkX + $cx, $chunkZ + $cz)){
1293 $cache[$chunkHash] = false;
1294 return false;
1295 }
1296 $adjacentChunk = $this->getChunk($chunkX + $cx, $chunkZ + $cz);
1297 if($adjacentChunk === null || !$adjacentChunk->isPopulated()){
1298 $cache[$chunkHash] = false;
1299 return false;
1300 }
1301 $lightPopulatedState = $adjacentChunk->isLightPopulated();
1302 if($lightPopulatedState !== true){
1303 if($lightPopulatedState === false){
1304 $this->orderLightPopulation($chunkX + $cx, $chunkZ + $cz);
1305 }
1306 $cache[$chunkHash] = false;
1307 return false;
1308 }
1309
1310 $cache[$chunkHash] = true;
1311 }
1312 }
1313
1314 return true;
1315 }
1316
1326 private function markTickingChunkForRecheck(int $chunkX, int $chunkZ) : void{
1327 for($cx = -1; $cx <= 1; ++$cx){
1328 for($cz = -1; $cz <= 1; ++$cz){
1329 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1330 unset($this->validTickingChunks[$chunkHash]);
1331 if(isset($this->registeredTickingChunks[$chunkHash])){
1332 $this->recheckTickingChunks[$chunkHash] = $chunkHash;
1333 }else{
1334 unset($this->recheckTickingChunks[$chunkHash]);
1335 }
1336 }
1337 }
1338 }
1339
1340 private function orderLightPopulation(int $chunkX, int $chunkZ) : void{
1341 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1342 $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated();
1343 if($lightPopulatedState === false){
1344 $this->chunks[$chunkHash]->setLightPopulated(null);
1345 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1346
1347 $this->workerPool->submitTask(new LightPopulationTask(
1348 $this->chunks[$chunkHash],
1349 function(array $blockLight, array $skyLight, array $heightMap) use ($chunkX, $chunkZ) : void{
1356 if($this->unloaded || ($chunk = $this->getChunk($chunkX, $chunkZ)) === null || $chunk->isLightPopulated() === true){
1357 return;
1358 }
1359 //TODO: calculated light information might not be valid if the terrain changed during light calculation
1360
1361 $chunk->setHeightMapArray($heightMap);
1362 foreach($blockLight as $y => $lightArray){
1363 $chunk->getSubChunk($y)->setBlockLightArray($lightArray);
1364 }
1365 foreach($skyLight as $y => $lightArray){
1366 $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray);
1367 }
1368 $chunk->setLightPopulated(true);
1369 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1370 }
1371 ));
1372 }
1373 }
1374
1375 private function tickChunk(int $chunkX, int $chunkZ) : void{
1376 $chunk = $this->getChunk($chunkX, $chunkZ);
1377 if($chunk === null){
1378 //the chunk may have been unloaded during a previous chunk's update (e.g. during BlockGrowEvent)
1379 return;
1380 }
1381 foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){
1382 $entity->onRandomUpdate();
1383 }
1384
1385 $blockFactory = $this->blockStateRegistry;
1386 foreach($chunk->getSubChunks() as $Y => $subChunk){
1387 if(!$subChunk->isEmptyFast()){
1388 $k = 0;
1389 for($i = 0; $i < $this->tickedBlocksPerSubchunkPerTick; ++$i){
1390 if(($i % 5) === 0){
1391 //60 bits will be used by 5 blocks (12 bits each)
1392 $k = mt_rand(0, (1 << 60) - 1);
1393 }
1394 $x = $k & SubChunk::COORD_MASK;
1395 $y = ($k >> SubChunk::COORD_BIT_SIZE) & SubChunk::COORD_MASK;
1396 $z = ($k >> (SubChunk::COORD_BIT_SIZE * 2)) & SubChunk::COORD_MASK;
1397 $k >>= (SubChunk::COORD_BIT_SIZE * 3);
1398
1399 $state = $subChunk->getBlockStateId($x, $y, $z);
1400
1401 if(isset($this->randomTickBlocks[$state])){
1402 $block = $blockFactory->fromStateId($state);
1403 $block->position($this, $chunkX * Chunk::EDGE_LENGTH + $x, ($Y << SubChunk::COORD_BIT_SIZE) + $y, $chunkZ * Chunk::EDGE_LENGTH + $z);
1404 $block->onRandomTick();
1405 }
1406 }
1407 }
1408 }
1409 }
1410
1414 public function __debugInfo() : array{
1415 return [];
1416 }
1417
1418 public function save(bool $force = false) : bool{
1419
1420 if(!$this->getAutoSave() && !$force){
1421 return false;
1422 }
1423
1424 (new WorldSaveEvent($this))->call();
1425
1426 $timings = $this->timings->syncDataSave;
1427 $timings->startTiming();
1428
1429 $this->provider->getWorldData()->setTime($this->time);
1430 $this->saveChunks();
1431 $this->provider->getWorldData()->save();
1432
1433 $timings->stopTiming();
1434
1435 return true;
1436 }
1437
1438 public function saveChunks() : void{
1439 $this->timings->syncChunkSave->startTiming();
1440 try{
1441 foreach($this->chunks as $chunkHash => $chunk){
1442 self::getXZ($chunkHash, $chunkX, $chunkZ);
1443 $this->provider->saveChunk($chunkX, $chunkZ, new ChunkData(
1444 $chunk->getSubChunks(),
1445 $chunk->isPopulated(),
1446 array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk()))),
1447 array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())),
1448 ), $chunk->getTerrainDirtyFlags());
1449 $chunk->clearTerrainDirtyFlags();
1450 }
1451 }finally{
1452 $this->timings->syncChunkSave->stopTiming();
1453 }
1454 }
1455
1460 public function scheduleDelayedBlockUpdate(Vector3 $pos, int $delay) : void{
1461 if(
1462 !$this->isInWorld($pos->x, $pos->y, $pos->z) ||
1463 (isset($this->scheduledBlockUpdateQueueIndex[$index = World::blockHash($pos->x, $pos->y, $pos->z)]) && $this->scheduledBlockUpdateQueueIndex[$index] <= $delay)
1464 ){
1465 return;
1466 }
1467 $this->scheduledBlockUpdateQueueIndex[$index] = $delay;
1468 $this->scheduledBlockUpdateQueue->insert(new Vector3((int) $pos->x, (int) $pos->y, (int) $pos->z), $delay + $this->server->getTick());
1469 }
1470
1471 private function tryAddToNeighbourUpdateQueue(int $x, int $y, int $z) : void{
1472 if($this->isInWorld($x, $y, $z)){
1473 $hash = World::blockHash($x, $y, $z);
1474 if(!isset($this->neighbourBlockUpdateQueueIndex[$hash])){
1475 $this->neighbourBlockUpdateQueue->enqueue($hash);
1476 $this->neighbourBlockUpdateQueueIndex[$hash] = true;
1477 }
1478 }
1479 }
1480
1487 private function internalNotifyNeighbourBlockUpdate(int $x, int $y, int $z) : void{
1488 $this->tryAddToNeighbourUpdateQueue($x, $y, $z);
1489 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1490 $this->tryAddToNeighbourUpdateQueue($x + $dx, $y + $dy, $z + $dz);
1491 }
1492 }
1493
1501 public function notifyNeighbourBlockUpdate(Vector3 $pos) : void{
1502 $this->internalNotifyNeighbourBlockUpdate($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ());
1503 }
1504
1509 public function getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst = false) : array{
1510 $minX = (int) floor($bb->minX - 1);
1511 $minY = (int) floor($bb->minY - 1);
1512 $minZ = (int) floor($bb->minZ - 1);
1513 $maxX = (int) floor($bb->maxX + 1);
1514 $maxY = (int) floor($bb->maxY + 1);
1515 $maxZ = (int) floor($bb->maxZ + 1);
1516
1517 $collides = [];
1518
1519 $collisionInfo = $this->blockStateRegistry->collisionInfo;
1520 if($targetFirst){
1521 for($z = $minZ; $z <= $maxZ; ++$z){
1522 $zOverflow = $z === $minZ || $z === $maxZ;
1523 for($x = $minX; $x <= $maxX; ++$x){
1524 $zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
1525 for($y = $minY; $y <= $maxY; ++$y){
1526 $overflow = $zxOverflow || $y === $minY || $y === $maxY;
1527
1528 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1529 if($overflow ?
1530 $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
1531 match ($stateCollisionInfo) {
1532 RuntimeBlockStateRegistry::COLLISION_CUBE => true,
1533 RuntimeBlockStateRegistry::COLLISION_NONE => false,
1534 default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
1535 }
1536 ){
1537 return [$this->getBlockAt($x, $y, $z)];
1538 }
1539 }
1540 }
1541 }
1542 }else{
1543 //TODO: duplicated code :( this way is better for performance though
1544 for($z = $minZ; $z <= $maxZ; ++$z){
1545 $zOverflow = $z === $minZ || $z === $maxZ;
1546 for($x = $minX; $x <= $maxX; ++$x){
1547 $zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
1548 for($y = $minY; $y <= $maxY; ++$y){
1549 $overflow = $zxOverflow || $y === $minY || $y === $maxY;
1550
1551 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1552 if($overflow ?
1553 $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
1554 match ($stateCollisionInfo) {
1555 RuntimeBlockStateRegistry::COLLISION_CUBE => true,
1556 RuntimeBlockStateRegistry::COLLISION_NONE => false,
1557 default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
1558 }
1559 ){
1560 $collides[] = $this->getBlockAt($x, $y, $z);
1561 }
1562 }
1563 }
1564 }
1565 }
1566
1567 return $collides;
1568 }
1569
1574 private function getBlockCollisionInfo(int $x, int $y, int $z, array $collisionInfo) : int{
1575 if(!$this->isInWorld($x, $y, $z)){
1576 return RuntimeBlockStateRegistry::COLLISION_NONE;
1577 }
1578 $chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1579 if($chunk === null){
1580 return RuntimeBlockStateRegistry::COLLISION_NONE;
1581 }
1582 $stateId = $chunk
1583 ->getSubChunk($y >> SubChunk::COORD_BIT_SIZE)
1584 ->getBlockStateId(
1585 $x & SubChunk::COORD_MASK,
1586 $y & SubChunk::COORD_MASK,
1587 $z & SubChunk::COORD_MASK
1588 );
1589 return $collisionInfo[$stateId];
1590 }
1591
1603 private function getBlockCollisionBoxesForCell(int $x, int $y, int $z, array $collisionInfo) : array{
1604 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1605 $boxes = match($stateCollisionInfo){
1606 RuntimeBlockStateRegistry::COLLISION_NONE => [],
1607 RuntimeBlockStateRegistry::COLLISION_CUBE => [AxisAlignedBB::one()->offset($x, $y, $z)],
1608 default => $this->getBlockAt($x, $y, $z)->getCollisionBoxes()
1609 };
1610
1611 //overlapping AABBs can't make any difference if this is a cube, so we can save some CPU cycles in this common case
1612 if($stateCollisionInfo !== RuntimeBlockStateRegistry::COLLISION_CUBE){
1613 $cellBB = null;
1614 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1615 $offsetX = $x + $dx;
1616 $offsetY = $y + $dy;
1617 $offsetZ = $z + $dz;
1618 $stateCollisionInfo = $this->getBlockCollisionInfo($offsetX, $offsetY, $offsetZ, $collisionInfo);
1619 if($stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW){
1620 //avoid allocating this unless it's needed
1621 $cellBB ??= AxisAlignedBB::one()->offset($x, $y, $z);
1622 $extraBoxes = $this->getBlockAt($offsetX, $offsetY, $offsetZ)->getCollisionBoxes();
1623 foreach($extraBoxes as $extraBox){
1624 if($extraBox->intersectsWith($cellBB)){
1625 $boxes[] = $extraBox;
1626 }
1627 }
1628 }
1629 }
1630 }
1631
1632 return $boxes;
1633 }
1634
1639 public function getBlockCollisionBoxes(AxisAlignedBB $bb) : array{
1640 $minX = (int) floor($bb->minX);
1641 $minY = (int) floor($bb->minY);
1642 $minZ = (int) floor($bb->minZ);
1643 $maxX = (int) floor($bb->maxX);
1644 $maxY = (int) floor($bb->maxY);
1645 $maxZ = (int) floor($bb->maxZ);
1646
1647 $collides = [];
1648
1649 $collisionInfo = $this->blockStateRegistry->collisionInfo;
1650
1651 for($z = $minZ; $z <= $maxZ; ++$z){
1652 for($x = $minX; $x <= $maxX; ++$x){
1653 $chunkPosHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1654 for($y = $minY; $y <= $maxY; ++$y){
1655 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1656
1657 $boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z, $collisionInfo);
1658
1659 foreach($boxes as $blockBB){
1660 if($blockBB->intersectsWith($bb)){
1661 $collides[] = $blockBB;
1662 }
1663 }
1664 }
1665 }
1666 }
1667
1668 return $collides;
1669 }
1670
1675 public function computeSunAnglePercentage() : float{
1676 $timeProgress = ($this->time % self::TIME_FULL) / self::TIME_FULL;
1677
1678 //0.0 needs to be high noon, not dusk
1679 $sunProgress = $timeProgress + ($timeProgress < 0.25 ? 0.75 : -0.25);
1680
1681 //Offset the sun progress to be above the horizon longer at dusk and dawn
1682 //this is roughly an inverted sine curve, which pushes the sun progress back at dusk and forwards at dawn
1683 $diff = (((1 - ((cos($sunProgress * M_PI) + 1) / 2)) - $sunProgress) / 3);
1684
1685 return $sunProgress + $diff;
1686 }
1687
1691 public function getSunAnglePercentage() : float{
1692 return $this->sunAnglePercentage;
1693 }
1694
1698 public function getSunAngleRadians() : float{
1699 return $this->sunAnglePercentage * 2 * M_PI;
1700 }
1701
1705 public function getSunAngleDegrees() : float{
1706 return $this->sunAnglePercentage * 360.0;
1707 }
1708
1713 public function computeSkyLightReduction() : int{
1714 $percentage = max(0, min(1, -(cos($this->getSunAngleRadians()) * 2 - 0.5)));
1715
1716 //TODO: check rain and thunder level
1717
1718 return (int) ($percentage * 11);
1719 }
1720
1724 public function getSkyLightReduction() : int{
1725 return $this->skyLightReduction;
1726 }
1727
1732 public function getFullLight(Vector3 $pos) : int{
1733 $floorX = $pos->getFloorX();
1734 $floorY = $pos->getFloorY();
1735 $floorZ = $pos->getFloorZ();
1736 return $this->getFullLightAt($floorX, $floorY, $floorZ);
1737 }
1738
1743 public function getFullLightAt(int $x, int $y, int $z) : int{
1744 $skyLight = $this->getRealBlockSkyLightAt($x, $y, $z);
1745 if($skyLight < 15){
1746 return max($skyLight, $this->getBlockLightAt($x, $y, $z));
1747 }else{
1748 return $skyLight;
1749 }
1750 }
1751
1756 public function getHighestAdjacentFullLightAt(int $x, int $y, int $z) : int{
1757 return $this->getHighestAdjacentLight($x, $y, $z, $this->getFullLightAt(...));
1758 }
1759
1764 public function getPotentialLight(Vector3 $pos) : int{
1765 $floorX = $pos->getFloorX();
1766 $floorY = $pos->getFloorY();
1767 $floorZ = $pos->getFloorZ();
1768 return $this->getPotentialLightAt($floorX, $floorY, $floorZ);
1769 }
1770
1775 public function getPotentialLightAt(int $x, int $y, int $z) : int{
1776 return max($this->getPotentialBlockSkyLightAt($x, $y, $z), $this->getBlockLightAt($x, $y, $z));
1777 }
1778
1783 public function getHighestAdjacentPotentialLightAt(int $x, int $y, int $z) : int{
1784 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialLightAt(...));
1785 }
1786
1793 public function getPotentialBlockSkyLightAt(int $x, int $y, int $z) : int{
1794 if(!$this->isInWorld($x, $y, $z)){
1795 return $y >= self::Y_MAX ? 15 : 0;
1796 }
1797 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){
1798 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockSkyLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1799 }
1800 return 0; //TODO: this should probably throw instead (light not calculated yet)
1801 }
1802
1808 public function getRealBlockSkyLightAt(int $x, int $y, int $z) : int{
1809 $light = $this->getPotentialBlockSkyLightAt($x, $y, $z) - $this->skyLightReduction;
1810 return $light < 0 ? 0 : $light;
1811 }
1812
1818 public function getBlockLightAt(int $x, int $y, int $z) : int{
1819 if(!$this->isInWorld($x, $y, $z)){
1820 return 0;
1821 }
1822 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){
1823 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1824 }
1825 return 0; //TODO: this should probably throw instead (light not calculated yet)
1826 }
1827
1828 public function updateAllLight(int $x, int $y, int $z) : void{
1829 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) === null || $chunk->isLightPopulated() !== true){
1830 return;
1831 }
1832
1833 $blockFactory = $this->blockStateRegistry;
1834 $this->timings->doBlockSkyLightUpdates->startTiming();
1835 if($this->skyLightUpdate === null){
1836 $this->skyLightUpdate = new SkyLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight);
1837 }
1838 $this->skyLightUpdate->recalculateNode($x, $y, $z);
1839 $this->timings->doBlockSkyLightUpdates->stopTiming();
1840
1841 $this->timings->doBlockLightUpdates->startTiming();
1842 if($this->blockLightUpdate === null){
1843 $this->blockLightUpdate = new BlockLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->light);
1844 }
1845 $this->blockLightUpdate->recalculateNode($x, $y, $z);
1846 $this->timings->doBlockLightUpdates->stopTiming();
1847 }
1848
1852 private function getHighestAdjacentLight(int $x, int $y, int $z, \Closure $lightGetter) : int{
1853 $max = 0;
1854 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
1855 $x1 = $x + $offsetX;
1856 $y1 = $y + $offsetY;
1857 $z1 = $z + $offsetZ;
1858 if(
1859 !$this->isInWorld($x1, $y1, $z1) ||
1860 ($chunk = $this->getChunk($x1 >> Chunk::COORD_BIT_SIZE, $z1 >> Chunk::COORD_BIT_SIZE)) === null ||
1861 $chunk->isLightPopulated() !== true
1862 ){
1863 continue;
1864 }
1865 $max = max($max, $lightGetter($x1, $y1, $z1));
1866 }
1867 return $max;
1868 }
1869
1873 public function getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z) : int{
1874 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialBlockSkyLightAt(...));
1875 }
1876
1881 public function getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z) : int{
1882 return $this->getHighestAdjacentPotentialBlockSkyLight($x, $y, $z) - $this->skyLightReduction;
1883 }
1884
1888 public function getHighestAdjacentBlockLight(int $x, int $y, int $z) : int{
1889 return $this->getHighestAdjacentLight($x, $y, $z, $this->getBlockLightAt(...));
1890 }
1891
1892 private function executeQueuedLightUpdates() : void{
1893 if($this->blockLightUpdate !== null){
1894 $this->timings->doBlockLightUpdates->startTiming();
1895 $this->blockLightUpdate->execute();
1896 $this->blockLightUpdate = null;
1897 $this->timings->doBlockLightUpdates->stopTiming();
1898 }
1899
1900 if($this->skyLightUpdate !== null){
1901 $this->timings->doBlockSkyLightUpdates->startTiming();
1902 $this->skyLightUpdate->execute();
1903 $this->skyLightUpdate = null;
1904 $this->timings->doBlockSkyLightUpdates->stopTiming();
1905 }
1906 }
1907
1908 public function isInWorld(int $x, int $y, int $z) : bool{
1909 return (
1910 $x <= Limits::INT32_MAX && $x >= Limits::INT32_MIN &&
1911 $y < $this->maxY && $y >= $this->minY &&
1912 $z <= Limits::INT32_MAX && $z >= Limits::INT32_MIN
1913 );
1914 }
1915
1926 public function getBlock(Vector3 $pos, bool $cached = true, bool $addToCache = true) : Block{
1927 return $this->getBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $cached, $addToCache);
1928 }
1929
1939 public function getBlockAt(int $x, int $y, int $z, bool $cached = true, bool $addToCache = true) : Block{
1940 $relativeBlockHash = null;
1941 $chunkHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1942
1943 if($this->isInWorld($x, $y, $z)){
1944 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1945
1946 if($cached && isset($this->blockCache[$chunkHash][$relativeBlockHash])){
1947 return $this->blockCache[$chunkHash][$relativeBlockHash];
1948 }
1949
1950 $chunk = $this->chunks[$chunkHash] ?? null;
1951 if($chunk !== null){
1952 $block = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK));
1953 }else{
1954 $addToCache = false;
1955 $block = VanillaBlocks::AIR();
1956 }
1957 }else{
1958 $block = VanillaBlocks::AIR();
1959 }
1960
1961 $block->position($this, $x, $y, $z);
1962
1963 if($this->inDynamicStateRecalculation){
1964 //this call was generated by a parent getBlock() call calculating dynamic stateinfo
1965 //don't calculate dynamic state and don't add to block cache (since it won't have dynamic state calculated).
1966 //this ensures that it's impossible for dynamic state properties to recursively depend on each other.
1967 $addToCache = false;
1968 }else{
1969 $this->inDynamicStateRecalculation = true;
1970 $replacement = $block->readStateFromWorld();
1971 if($replacement !== $block){
1972 $replacement->position($this, $x, $y, $z);
1973 $block = $replacement;
1974 }
1975 $this->inDynamicStateRecalculation = false;
1976 }
1977
1978 if($addToCache && $relativeBlockHash !== null){
1979 $this->blockCache[$chunkHash][$relativeBlockHash] = $block;
1980
1981 if(++$this->blockCacheSize >= self::BLOCK_CACHE_SIZE_CAP){
1982 $this->trimBlockCache();
1983 }
1984 }
1985
1986 return $block;
1987 }
1988
1994 public function setBlock(Vector3 $pos, Block $block, bool $update = true) : void{
1995 $this->setBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $block, $update);
1996 }
1997
2006 public function setBlockAt(int $x, int $y, int $z, Block $block, bool $update = true) : void{
2007 if(!$this->isInWorld($x, $y, $z)){
2008 throw new \InvalidArgumentException("Pos x=$x,y=$y,z=$z is outside of the world bounds");
2009 }
2010 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
2011 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
2012 if($this->loadChunk($chunkX, $chunkZ) === null){ //current expected behaviour is to try to load the terrain synchronously
2013 throw new WorldException("Cannot set a block in un-generated terrain");
2014 }
2015
2016 //TODO: this computes state ID twice (we do it again in writeStateToWorld()). Not great for performance :(
2017 $stateId = $block->getStateId();
2018 if(!$this->blockStateRegistry->hasStateId($stateId)){
2019 throw new \LogicException("Block state ID not known to RuntimeBlockStateRegistry (probably not registered)");
2020 }
2021 if(!GlobalBlockStateHandlers::getSerializer()->isRegistered($block)){
2022 throw new \LogicException("Block not registered with GlobalBlockStateHandlers serializer");
2023 }
2024
2025 $this->timings->setBlock->startTiming();
2026
2027 $this->unlockChunk($chunkX, $chunkZ, null);
2028
2029 $block = clone $block;
2030
2031 $block->position($this, $x, $y, $z);
2032 $block->writeStateToWorld();
2033 $pos = new Vector3($x, $y, $z);
2034
2035 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2036 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
2037
2038 unset($this->blockCache[$chunkHash][$relativeBlockHash]);
2039 $this->blockCacheSize--;
2040 unset($this->blockCollisionBoxCache[$chunkHash][$relativeBlockHash]);
2041 //blocks like fences have collision boxes that reach into neighbouring blocks, so we need to invalidate the
2042 //caches for those blocks as well
2043 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
2044 $sideChunkPosHash = World::chunkHash(($x + $offsetX) >> Chunk::COORD_BIT_SIZE, ($z + $offsetZ) >> Chunk::COORD_BIT_SIZE);
2045 $sideChunkBlockHash = World::chunkBlockHash($x + $offsetX, $y + $offsetY, $z + $offsetZ);
2046 unset($this->blockCollisionBoxCache[$sideChunkPosHash][$sideChunkBlockHash]);
2047 }
2048
2049 if(!isset($this->changedBlocks[$chunkHash])){
2050 $this->changedBlocks[$chunkHash] = [];
2051 }
2052 $this->changedBlocks[$chunkHash][$relativeBlockHash] = $pos;
2053
2054 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2055 $listener->onBlockChanged($pos);
2056 }
2057
2058 if($update){
2059 $this->updateAllLight($x, $y, $z);
2060 $this->internalNotifyNeighbourBlockUpdate($x, $y, $z);
2061 }
2062
2063 $this->timings->setBlock->stopTiming();
2064 }
2065
2066 public function dropItem(Vector3 $source, Item $item, ?Vector3 $motion = null, int $delay = 10) : ?ItemEntity{
2067 if($item->isNull()){
2068 return null;
2069 }
2070
2071 $itemEntity = new ItemEntity(Location::fromObject($source, $this, Utils::getRandomFloat() * 360, 0), $item);
2072
2073 $itemEntity->setPickupDelay($delay);
2074 $itemEntity->setMotion($motion ?? new Vector3(Utils::getRandomFloat() * 0.2 - 0.1, 0.2, Utils::getRandomFloat() * 0.2 - 0.1));
2075 $itemEntity->spawnToAll();
2076
2077 return $itemEntity;
2078 }
2079
2086 public function dropExperience(Vector3 $pos, int $amount) : array{
2087 $orbs = [];
2088
2089 foreach(ExperienceOrb::splitIntoOrbSizes($amount) as $split){
2090 $orb = new ExperienceOrb(Location::fromObject($pos, $this, Utils::getRandomFloat() * 360, 0), $split);
2091
2092 $orb->setMotion(new Vector3((Utils::getRandomFloat() * 0.2 - 0.1) * 2, Utils::getRandomFloat() * 0.4, (Utils::getRandomFloat() * 0.2 - 0.1) * 2));
2093 $orb->spawnToAll();
2094
2095 $orbs[] = $orb;
2096 }
2097
2098 return $orbs;
2099 }
2100
2109 public function useBreakOn(Vector3 $vector, ?Item &$item = null, ?Player $player = null, bool $createParticles = false, array &$returnedItems = []) : bool{
2110 $vector = $vector->floor();
2111
2112 $chunkX = $vector->getFloorX() >> Chunk::COORD_BIT_SIZE;
2113 $chunkZ = $vector->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2114 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2115 return false;
2116 }
2117
2118 $target = $this->getBlock($vector);
2119 $affectedBlocks = $target->getAffectedBlocks();
2120
2121 if($item === null){
2122 $item = VanillaItems::AIR();
2123 }
2124
2125 $drops = [];
2126 if($player === null || $player->hasFiniteResources()){
2127 $drops = array_merge(...array_map(fn(Block $block) => $block->getDrops($item), $affectedBlocks));
2128 }
2129
2130 $xpDrop = 0;
2131 if($player !== null && $player->hasFiniteResources()){
2132 $xpDrop = array_sum(array_map(fn(Block $block) => $block->getXpDropForTool($item), $affectedBlocks));
2133 }
2134
2135 if($player !== null){
2136 $ev = new BlockBreakEvent($player, $target, $item, $player->isCreative(), $drops, $xpDrop);
2137
2138 if($target instanceof Air || ($player->isSurvival() && !$target->getBreakInfo()->isBreakable()) || $player->isSpectator()){
2139 $ev->cancel();
2140 }
2141
2142 if($player->isAdventure(true) && !$ev->isCancelled()){
2143 $canBreak = false;
2144 $itemParser = LegacyStringToItemParser::getInstance();
2145 foreach($item->getCanDestroy() as $v){
2146 $entry = $itemParser->parse($v);
2147 if($entry->getBlock()->hasSameTypeId($target)){
2148 $canBreak = true;
2149 break;
2150 }
2151 }
2152
2153 if(!$canBreak){
2154 $ev->cancel();
2155 }
2156 }
2157
2158 $ev->call();
2159 if($ev->isCancelled()){
2160 return false;
2161 }
2162
2163 $drops = $ev->getDrops();
2164 $xpDrop = $ev->getXpDropAmount();
2165
2166 }elseif(!$target->getBreakInfo()->isBreakable()){
2167 return false;
2168 }
2169
2170 foreach($affectedBlocks as $t){
2171 $this->destroyBlockInternal($t, $item, $player, $createParticles, $returnedItems);
2172 }
2173
2174 $item->onDestroyBlock($target, $returnedItems);
2175
2176 if(count($drops) > 0){
2177 $dropPos = $vector->add(0.5, 0.5, 0.5);
2178 foreach($drops as $drop){
2179 if(!$drop->isNull()){
2180 $this->dropItem($dropPos, $drop);
2181 }
2182 }
2183 }
2184
2185 if($xpDrop > 0){
2186 $this->dropExperience($vector->add(0.5, 0.5, 0.5), $xpDrop);
2187 }
2188
2189 return true;
2190 }
2191
2195 private function destroyBlockInternal(Block $target, Item $item, ?Player $player, bool $createParticles, array &$returnedItems) : void{
2196 if($createParticles){
2197 $this->addParticle($target->getPosition()->add(0.5, 0.5, 0.5), new BlockBreakParticle($target));
2198 }
2199
2200 $target->onBreak($item, $player, $returnedItems);
2201
2202 $tile = $this->getTile($target->getPosition());
2203 if($tile !== null){
2204 $tile->onBlockDestroyed();
2205 }
2206 }
2207
2215 public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector = null, ?Player $player = null, bool $playSound = false, array &$returnedItems = []) : bool{
2216 $blockClicked = $this->getBlock($vector);
2217 $blockReplace = $blockClicked->getSide($face);
2218
2219 if($clickVector === null){
2220 $clickVector = new Vector3(0.0, 0.0, 0.0);
2221 }else{
2222 $clickVector = new Vector3(
2223 min(1.0, max(0.0, $clickVector->x)),
2224 min(1.0, max(0.0, $clickVector->y)),
2225 min(1.0, max(0.0, $clickVector->z))
2226 );
2227 }
2228
2229 if(!$this->isInWorld($blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z)){
2230 //TODO: build height limit messages for custom world heights and mcregion cap
2231 return false;
2232 }
2233 $chunkX = $blockReplace->getPosition()->getFloorX() >> Chunk::COORD_BIT_SIZE;
2234 $chunkZ = $blockReplace->getPosition()->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2235 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2236 return false;
2237 }
2238
2239 if($blockClicked->getTypeId() === BlockTypeIds::AIR){
2240 return false;
2241 }
2242
2243 if($player !== null){
2244 $ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK);
2245 if($player->isSneaking()){
2246 $ev->setUseItem(false);
2247 $ev->setUseBlock($item->isNull()); //opening doors is still possible when sneaking if using an empty hand
2248 }
2249 if($player->isSpectator()){
2250 $ev->cancel(); //set it to cancelled so plugins can bypass this
2251 }
2252
2253 $ev->call();
2254 if(!$ev->isCancelled()){
2255 if($ev->useBlock() && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2256 return true;
2257 }
2258
2259 if($ev->useItem()){
2260 $result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems);
2261 if($result !== ItemUseResult::NONE){
2262 return $result === ItemUseResult::SUCCESS;
2263 }
2264 }
2265 }else{
2266 return false;
2267 }
2268 }elseif($blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2269 return true;
2270 }
2271
2272 if($item->isNull() || !$item->canBePlaced()){
2273 return false;
2274 }
2275 $hand = $item->getBlock($face);
2276 $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z);
2277
2278 if($hand->canBePlacedAt($blockClicked, $clickVector, $face, true)){
2279 $blockReplace = $blockClicked;
2280 //TODO: while this mimics the vanilla behaviour with replaceable blocks, we should really pass some other
2281 //value like NULL and let place() deal with it. This will look like a bug to anyone who doesn't know about
2282 //the vanilla behaviour.
2283 $face = Facing::UP;
2284 $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z);
2285 }elseif(!$hand->canBePlacedAt($blockReplace, $clickVector, $face, false)){
2286 return false;
2287 }
2288
2289 $tx = new BlockTransaction($this);
2290 if(!$hand->place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player)){
2291 return false;
2292 }
2293
2294 foreach($tx->getBlocks() as [$x, $y, $z, $block]){
2295 $block->position($this, $x, $y, $z);
2296 foreach($block->getCollisionBoxes() as $collisionBox){
2297 if(count($this->getCollidingEntities($collisionBox)) > 0){
2298 return false; //Entity in block
2299 }
2300 }
2301 }
2302
2303 if($player !== null){
2304 $ev = new BlockPlaceEvent($player, $tx, $blockClicked, $item);
2305 if($player->isSpectator()){
2306 $ev->cancel();
2307 }
2308
2309 if($player->isAdventure(true) && !$ev->isCancelled()){
2310 $canPlace = false;
2311 $itemParser = LegacyStringToItemParser::getInstance();
2312 foreach($item->getCanPlaceOn() as $v){
2313 $entry = $itemParser->parse($v);
2314 if($entry->getBlock()->hasSameTypeId($blockClicked)){
2315 $canPlace = true;
2316 break;
2317 }
2318 }
2319
2320 if(!$canPlace){
2321 $ev->cancel();
2322 }
2323 }
2324
2325 $ev->call();
2326 if($ev->isCancelled()){
2327 return false;
2328 }
2329 }
2330
2331 if(!$tx->apply()){
2332 return false;
2333 }
2334 foreach($tx->getBlocks() as [$x, $y, $z, $_]){
2335 $tile = $this->getTileAt($x, $y, $z);
2336 if($tile !== null){
2337 //TODO: seal this up inside block placement
2338 $tile->copyDataFromItem($item);
2339 }
2340
2341 $this->getBlockAt($x, $y, $z)->onPostPlace();
2342 }
2343
2344 if($playSound){
2345 $this->addSound($hand->getPosition(), new BlockPlaceSound($hand));
2346 }
2347
2348 $item->pop();
2349
2350 return true;
2351 }
2352
2353 public function getEntity(int $entityId) : ?Entity{
2354 return $this->entities[$entityId] ?? null;
2355 }
2356
2363 public function getEntities() : array{
2364 return $this->entities;
2365 }
2366
2377 public function getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
2378 $nearby = [];
2379
2380 foreach($this->getNearbyEntities($bb, $entity) as $ent){
2381 if($ent->canBeCollidedWith() && ($entity === null || $entity->canCollideWith($ent))){
2382 $nearby[] = $ent;
2383 }
2384 }
2385
2386 return $nearby;
2387 }
2388
2395 public function getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
2396 $nearby = [];
2397
2398 $minX = ((int) floor($bb->minX - 2)) >> Chunk::COORD_BIT_SIZE;
2399 $maxX = ((int) floor($bb->maxX + 2)) >> Chunk::COORD_BIT_SIZE;
2400 $minZ = ((int) floor($bb->minZ - 2)) >> Chunk::COORD_BIT_SIZE;
2401 $maxZ = ((int) floor($bb->maxZ + 2)) >> Chunk::COORD_BIT_SIZE;
2402
2403 for($x = $minX; $x <= $maxX; ++$x){
2404 for($z = $minZ; $z <= $maxZ; ++$z){
2405 foreach($this->getChunkEntities($x, $z) as $ent){
2406 if($ent !== $entity && $ent->boundingBox->intersectsWith($bb)){
2407 $nearby[] = $ent;
2408 }
2409 }
2410 }
2411 }
2412
2413 return $nearby;
2414 }
2415
2427 public function getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType = Entity::class, bool $includeDead = false) : ?Entity{
2428 assert(is_a($entityType, Entity::class, true));
2429
2430 $minX = ((int) floor($pos->x - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2431 $maxX = ((int) floor($pos->x + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2432 $minZ = ((int) floor($pos->z - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2433 $maxZ = ((int) floor($pos->z + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2434
2435 $currentTargetDistSq = $maxDistance ** 2;
2436
2441 $currentTarget = null;
2442
2443 for($x = $minX; $x <= $maxX; ++$x){
2444 for($z = $minZ; $z <= $maxZ; ++$z){
2445 foreach($this->getChunkEntities($x, $z) as $entity){
2446 if(!($entity instanceof $entityType) || $entity->isFlaggedForDespawn() || (!$includeDead && !$entity->isAlive())){
2447 continue;
2448 }
2449 $distSq = $entity->getPosition()->distanceSquared($pos);
2450 if($distSq < $currentTargetDistSq){
2451 $currentTargetDistSq = $distSq;
2452 $currentTarget = $entity;
2453 }
2454 }
2455 }
2456 }
2457
2458 return $currentTarget;
2459 }
2460
2467 public function getPlayers() : array{
2468 return $this->players;
2469 }
2470
2477 public function getTile(Vector3 $pos) : ?Tile{
2478 return $this->getTileAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z));
2479 }
2480
2484 public function getTileAt(int $x, int $y, int $z) : ?Tile{
2485 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;
2486 }
2487
2488 public function getBiomeId(int $x, int $y, int $z) : int{
2489 if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
2490 return $chunk->getBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2491 }
2492 return BiomeIds::OCEAN; //TODO: this should probably throw instead (terrain not generated yet)
2493 }
2494
2495 public function getBiome(int $x, int $y, int $z) : Biome{
2496 return BiomeRegistry::getInstance()->getBiome($this->getBiomeId($x, $y, $z));
2497 }
2498
2499 public function setBiomeId(int $x, int $y, int $z, int $biomeId) : void{
2500 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
2501 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
2502 $this->unlockChunk($chunkX, $chunkZ, null);
2503 if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
2504 $chunk->setBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId);
2505 }else{
2506 //if we allowed this, the modifications would be lost when the chunk is created
2507 throw new WorldException("Cannot set biome in a non-generated chunk");
2508 }
2509 }
2510
2515 public function getLoadedChunks() : array{
2516 return $this->chunks;
2517 }
2518
2519 public function getChunk(int $chunkX, int $chunkZ) : ?Chunk{
2520 return $this->chunks[World::chunkHash($chunkX, $chunkZ)] ?? null;
2521 }
2522
2527 public function getChunkEntities(int $chunkX, int $chunkZ) : array{
2528 return $this->entitiesByChunk[World::chunkHash($chunkX, $chunkZ)] ?? [];
2529 }
2530
2534 public function getOrLoadChunkAtPosition(Vector3 $pos) : ?Chunk{
2535 return $this->loadChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2536 }
2537
2544 public function getAdjacentChunks(int $x, int $z) : array{
2545 $result = [];
2546 for($xx = -1; $xx <= 1; ++$xx){
2547 for($zz = -1; $zz <= 1; ++$zz){
2548 if($xx === 0 && $zz === 0){
2549 continue; //center chunk
2550 }
2551 $result[World::chunkHash($xx, $zz)] = $this->loadChunk($x + $xx, $z + $zz);
2552 }
2553 }
2554
2555 return $result;
2556 }
2557
2572 public function lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId) : void{
2573 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2574 if(isset($this->chunkLock[$chunkHash])){
2575 throw new \InvalidArgumentException("Chunk $chunkX $chunkZ is already locked");
2576 }
2577 $this->chunkLock[$chunkHash] = $lockId;
2578 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2579 }
2580
2589 public function unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId) : bool{
2590 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2591 if(isset($this->chunkLock[$chunkHash]) && ($lockId === null || $this->chunkLock[$chunkHash] === $lockId)){
2592 unset($this->chunkLock[$chunkHash]);
2593 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2594 return true;
2595 }
2596 return false;
2597 }
2598
2604 public function isChunkLocked(int $chunkX, int $chunkZ) : bool{
2605 return isset($this->chunkLock[World::chunkHash($chunkX, $chunkZ)]);
2606 }
2607
2608 public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk) : void{
2609 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2610 $oldChunk = $this->loadChunk($chunkX, $chunkZ);
2611 if($oldChunk !== null && $oldChunk !== $chunk){
2612 $deletedTiles = 0;
2613 $transferredTiles = 0;
2614 foreach($oldChunk->getTiles() as $oldTile){
2615 $tilePosition = $oldTile->getPosition();
2616 $localX = $tilePosition->getFloorX() & Chunk::COORD_MASK;
2617 $localY = $tilePosition->getFloorY();
2618 $localZ = $tilePosition->getFloorZ() & Chunk::COORD_MASK;
2619
2620 $newBlock = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($localX, $localY, $localZ));
2621 $expectedTileClass = $newBlock->getIdInfo()->getTileClass();
2622 if(
2623 $expectedTileClass === null || //new block doesn't expect a tile
2624 !($oldTile instanceof $expectedTileClass) || //new block expects a different tile
2625 (($newTile = $chunk->getTile($localX, $localY, $localZ)) !== null && $newTile !== $oldTile) //new chunk already has a different tile
2626 ){
2627 $oldTile->close();
2628 $deletedTiles++;
2629 }else{
2630 $transferredTiles++;
2631 $chunk->addTile($oldTile);
2632 $oldChunk->removeTile($oldTile);
2633 }
2634 }
2635 if($deletedTiles > 0 || $transferredTiles > 0){
2636 $this->logger->debug("Replacement of chunk $chunkX $chunkZ caused deletion of $deletedTiles obsolete/conflicted tiles, and transfer of $transferredTiles");
2637 }
2638 }
2639
2640 $this->chunks[$chunkHash] = $chunk;
2641
2642 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
2643 unset($this->blockCache[$chunkHash]);
2644 unset($this->blockCollisionBoxCache[$chunkHash]);
2645 unset($this->changedBlocks[$chunkHash]);
2646 $chunk->setTerrainDirty();
2647 $this->markTickingChunkForRecheck($chunkX, $chunkZ); //this replacement chunk may not meet the conditions for ticking
2648
2649 if(!$this->isChunkInUse($chunkX, $chunkZ)){
2650 $this->unloadChunkRequest($chunkX, $chunkZ);
2651 }
2652
2653 if($oldChunk === null){
2654 if(ChunkLoadEvent::hasHandlers()){
2655 (new ChunkLoadEvent($this, $chunkX, $chunkZ, $chunk, true))->call();
2656 }
2657
2658 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2659 $listener->onChunkLoaded($chunkX, $chunkZ, $chunk);
2660 }
2661 }else{
2662 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2663 $listener->onChunkChanged($chunkX, $chunkZ, $chunk);
2664 }
2665 }
2666
2667 for($cX = -1; $cX <= 1; ++$cX){
2668 for($cZ = -1; $cZ <= 1; ++$cZ){
2669 foreach($this->getChunkEntities($chunkX + $cX, $chunkZ + $cZ) as $entity){
2670 $entity->onNearbyBlockChange();
2671 }
2672 }
2673 }
2674 }
2675
2682 public function getHighestBlockAt(int $x, int $z) : ?int{
2683 if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
2684 return $chunk->getHighestBlockAt($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2685 }
2686 throw new WorldException("Cannot get highest block in an ungenerated chunk");
2687 }
2688
2692 public function isInLoadedTerrain(Vector3 $pos) : bool{
2693 return $this->isChunkLoaded($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2694 }
2695
2696 public function isChunkLoaded(int $x, int $z) : bool{
2697 return isset($this->chunks[World::chunkHash($x, $z)]);
2698 }
2699
2700 public function isChunkGenerated(int $x, int $z) : bool{
2701 return $this->loadChunk($x, $z) !== null;
2702 }
2703
2704 public function isChunkPopulated(int $x, int $z) : bool{
2705 $chunk = $this->loadChunk($x, $z);
2706 return $chunk !== null && $chunk->isPopulated();
2707 }
2708
2712 public function getSpawnLocation() : Position{
2713 return Position::fromObject($this->provider->getWorldData()->getSpawn(), $this);
2714 }
2715
2719 public function setSpawnLocation(Vector3 $pos) : void{
2720 $previousSpawn = $this->getSpawnLocation();
2721 $this->provider->getWorldData()->setSpawn($pos);
2722 (new SpawnChangeEvent($this, $previousSpawn))->call();
2723
2724 $location = Position::fromObject($pos, $this);
2725 foreach($this->players as $player){
2726 $player->getNetworkSession()->syncWorldSpawnPoint($location);
2727 }
2728 }
2729
2733 public function addEntity(Entity $entity) : void{
2734 if($entity->isClosed()){
2735 throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to world");
2736 }
2737 if($entity->getWorld() !== $this){
2738 throw new \InvalidArgumentException("Invalid Entity world");
2739 }
2740 if(array_key_exists($entity->getId(), $this->entities)){
2741 if($this->entities[$entity->getId()] === $entity){
2742 throw new \InvalidArgumentException("Entity " . $entity->getId() . " has already been added to this world");
2743 }else{
2744 throw new AssumptionFailedError("Found two different entities sharing entity ID " . $entity->getId());
2745 }
2746 }
2747 if(!EntityFactory::getInstance()->isRegistered($entity::class) && !$entity instanceof Player){
2748 //canSaveWithChunk is mutable, so that means it could be toggled after adding the entity and cause a crash
2749 //later on. Better we just force all entities to have a save ID, even if it might not be needed.
2750 throw new \LogicException("Entity " . $entity::class . " is not registered for a save ID in EntityFactory");
2751 }
2752 $pos = $entity->getPosition()->asVector3();
2753 $this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity;
2754 $this->entityLastKnownPositions[$entity->getId()] = $pos;
2755
2756 if($entity instanceof Player){
2757 $this->players[$entity->getId()] = $entity;
2758 }
2759 $this->entities[$entity->getId()] = $entity;
2760 }
2761
2767 public function removeEntity(Entity $entity) : void{
2768 if($entity->getWorld() !== $this){
2769 throw new \InvalidArgumentException("Invalid Entity world");
2770 }
2771 if(!array_key_exists($entity->getId(), $this->entities)){
2772 throw new \InvalidArgumentException("Entity is not tracked by this world (possibly already removed?)");
2773 }
2774 $pos = $this->entityLastKnownPositions[$entity->getId()];
2775 $chunkHash = World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2776 if(isset($this->entitiesByChunk[$chunkHash][$entity->getId()])){
2777 if(count($this->entitiesByChunk[$chunkHash]) === 1){
2778 unset($this->entitiesByChunk[$chunkHash]);
2779 }else{
2780 unset($this->entitiesByChunk[$chunkHash][$entity->getId()]);
2781 }
2782 }
2783 unset($this->entityLastKnownPositions[$entity->getId()]);
2784
2785 if($entity instanceof Player){
2786 unset($this->players[$entity->getId()]);
2787 $this->checkSleep();
2788 }
2789
2790 unset($this->entities[$entity->getId()]);
2791 unset($this->updateEntities[$entity->getId()]);
2792 }
2793
2797 public function onEntityMoved(Entity $entity) : void{
2798 if(!array_key_exists($entity->getId(), $this->entityLastKnownPositions)){
2799 //this can happen if the entity was teleported before addEntity() was called
2800 return;
2801 }
2802 $oldPosition = $this->entityLastKnownPositions[$entity->getId()];
2803 $newPosition = $entity->getPosition();
2804
2805 $oldChunkX = $oldPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2806 $oldChunkZ = $oldPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2807 $newChunkX = $newPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2808 $newChunkZ = $newPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2809
2810 if($oldChunkX !== $newChunkX || $oldChunkZ !== $newChunkZ){
2811 $oldChunkHash = World::chunkHash($oldChunkX, $oldChunkZ);
2812 if(isset($this->entitiesByChunk[$oldChunkHash][$entity->getId()])){
2813 if(count($this->entitiesByChunk[$oldChunkHash]) === 1){
2814 unset($this->entitiesByChunk[$oldChunkHash]);
2815 }else{
2816 unset($this->entitiesByChunk[$oldChunkHash][$entity->getId()]);
2817 }
2818 }
2819
2820 $newViewers = $this->getViewersForPosition($newPosition);
2821 foreach($entity->getViewers() as $player){
2822 if(!isset($newViewers[spl_object_id($player)])){
2823 $entity->despawnFrom($player);
2824 }else{
2825 unset($newViewers[spl_object_id($player)]);
2826 }
2827 }
2828 foreach($newViewers as $player){
2829 $entity->spawnTo($player);
2830 }
2831
2832 $newChunkHash = World::chunkHash($newChunkX, $newChunkZ);
2833 $this->entitiesByChunk[$newChunkHash][$entity->getId()] = $entity;
2834 }
2835 $this->entityLastKnownPositions[$entity->getId()] = $newPosition->asVector3();
2836 }
2837
2842 public function addTile(Tile $tile) : void{
2843 if($tile->isClosed()){
2844 throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to world");
2845 }
2846 $pos = $tile->getPosition();
2847 if(!$pos->isValid() || $pos->getWorld() !== $this){
2848 throw new \InvalidArgumentException("Invalid Tile world");
2849 }
2850 if(!$this->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ())){
2851 throw new \InvalidArgumentException("Tile position is outside the world bounds");
2852 }
2853 if(!TileFactory::getInstance()->isRegistered($tile::class)){
2854 throw new \LogicException("Tile " . $tile::class . " is not registered for a save ID in TileFactory");
2855 }
2856
2857 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2858 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2859
2860 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2861 $this->chunks[$hash]->addTile($tile);
2862 }else{
2863 throw new \InvalidArgumentException("Attempted to create tile " . get_class($tile) . " in unloaded chunk $chunkX $chunkZ");
2864 }
2865
2866 //delegate tile ticking to the corresponding block
2867 $this->scheduleDelayedBlockUpdate($pos->asVector3(), 1);
2868 }
2869
2874 public function removeTile(Tile $tile) : void{
2875 $pos = $tile->getPosition();
2876 if(!$pos->isValid() || $pos->getWorld() !== $this){
2877 throw new \InvalidArgumentException("Invalid Tile world");
2878 }
2879
2880 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2881 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2882
2883 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2884 $this->chunks[$hash]->removeTile($tile);
2885 }
2886 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2887 $listener->onBlockChanged($pos->asVector3());
2888 }
2889 }
2890
2891 public function isChunkInUse(int $x, int $z) : bool{
2892 return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) && count($this->chunkLoaders[$index]) > 0;
2893 }
2894
2901 public function loadChunk(int $x, int $z) : ?Chunk{
2902 if(isset($this->chunks[$chunkHash = World::chunkHash($x, $z)])){
2903 return $this->chunks[$chunkHash];
2904 }
2905
2906 $this->timings->syncChunkLoad->startTiming();
2907
2908 $this->cancelUnloadChunkRequest($x, $z);
2909
2910 $this->timings->syncChunkLoadData->startTiming();
2911
2912 $loadedChunkData = null;
2913
2914 try{
2915 $loadedChunkData = $this->provider->loadChunk($x, $z);
2916 }catch(CorruptedChunkException $e){
2917 $this->logger->critical("Failed to load chunk x=$x z=$z: " . $e->getMessage());
2918 }
2919
2920 $this->timings->syncChunkLoadData->stopTiming();
2921
2922 if($loadedChunkData === null){
2923 $this->timings->syncChunkLoad->stopTiming();
2924 return null;
2925 }
2926
2927 $chunkData = $loadedChunkData->getData();
2928 $chunk = new Chunk($chunkData->getSubChunks(), $chunkData->isPopulated());
2929 if(!$loadedChunkData->isUpgraded()){
2930 $chunk->clearTerrainDirtyFlags();
2931 }else{
2932 $this->logger->debug("Chunk $x $z has been upgraded, will be saved at the next autosave opportunity");
2933 }
2934 $this->chunks[$chunkHash] = $chunk;
2935
2936 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
2937 unset($this->blockCache[$chunkHash]);
2938 unset($this->blockCollisionBoxCache[$chunkHash]);
2939
2940 $this->initChunk($x, $z, $chunkData);
2941
2942 if(ChunkLoadEvent::hasHandlers()){
2943 (new ChunkLoadEvent($this, $x, $z, $this->chunks[$chunkHash], false))->call();
2944 }
2945
2946 if(!$this->isChunkInUse($x, $z)){
2947 $this->logger->debug("Newly loaded chunk $x $z has no loaders registered, will be unloaded at next available opportunity");
2948 $this->unloadChunkRequest($x, $z);
2949 }
2950 foreach($this->getChunkListeners($x, $z) as $listener){
2951 $listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]);
2952 }
2953 $this->markTickingChunkForRecheck($x, $z); //tickers may have been registered before the chunk was loaded
2954
2955 $this->timings->syncChunkLoad->stopTiming();
2956
2957 return $this->chunks[$chunkHash];
2958 }
2959
2960 private function initChunk(int $chunkX, int $chunkZ, ChunkData $chunkData) : void{
2961 $logger = new \PrefixedLogger($this->logger, "Loading chunk $chunkX $chunkZ");
2962
2963 if(count($chunkData->getEntityNBT()) !== 0){
2964 $this->timings->syncChunkLoadEntities->startTiming();
2965 $entityFactory = EntityFactory::getInstance();
2966 foreach($chunkData->getEntityNBT() as $k => $nbt){
2967 try{
2968 $entity = $entityFactory->createFromData($this, $nbt);
2969 }catch(SavedDataLoadingException $e){
2970 $logger->error("Bad entity data at list position $k: " . $e->getMessage());
2971 $logger->logException($e);
2972 continue;
2973 }
2974 if($entity === null){
2975 $saveIdTag = $nbt->getTag("identifier") ?? $nbt->getTag("id");
2976 $saveId = "<unknown>";
2977 if($saveIdTag instanceof StringTag){
2978 $saveId = $saveIdTag->getValue();
2979 }elseif($saveIdTag instanceof IntTag){ //legacy MCPE format
2980 $saveId = "legacy(" . $saveIdTag->getValue() . ")";
2981 }
2982 $logger->warning("Deleted unknown entity type $saveId");
2983 }
2984 //TODO: we can't prevent entities getting added to unloaded chunks if they were saved in the wrong place
2985 //here, because entities currently add themselves to the world
2986 }
2987
2988 $this->timings->syncChunkLoadEntities->stopTiming();
2989 }
2990
2991 if(count($chunkData->getTileNBT()) !== 0){
2992 $this->timings->syncChunkLoadTileEntities->startTiming();
2993 $tileFactory = TileFactory::getInstance();
2994 foreach($chunkData->getTileNBT() as $k => $nbt){
2995 try{
2996 $tile = $tileFactory->createFromData($this, $nbt);
2997 }catch(SavedDataLoadingException $e){
2998 $logger->error("Bad tile entity data at list position $k: " . $e->getMessage());
2999 $logger->logException($e);
3000 continue;
3001 }
3002 if($tile === null){
3003 $logger->warning("Deleted unknown tile entity type " . $nbt->getString("id", "<unknown>"));
3004 continue;
3005 }
3006
3007 $tilePosition = $tile->getPosition();
3008 if(!$this->isChunkLoaded($tilePosition->getFloorX() >> Chunk::COORD_BIT_SIZE, $tilePosition->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
3009 $logger->error("Found tile saved on wrong chunk - unable to fix due to correct chunk not loaded");
3010 }elseif(!$this->isInWorld($tilePosition->getFloorX(), $tilePosition->getFloorY(), $tilePosition->getFloorZ())){
3011 $logger->error("Cannot add tile with position outside the world bounds: x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z");
3012 }elseif($this->getTile($tilePosition) !== null){
3013 $logger->error("Cannot add tile at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z: Another tile is already at that position");
3014 }else{
3015 $this->addTile($tile);
3016 }
3017 }
3018
3019 $this->timings->syncChunkLoadTileEntities->stopTiming();
3020 }
3021 }
3022
3023 private function queueUnloadChunk(int $x, int $z) : void{
3024 $this->unloadQueue[World::chunkHash($x, $z)] = microtime(true);
3025 }
3026
3027 public function unloadChunkRequest(int $x, int $z, bool $safe = true) : bool{
3028 if(($safe && $this->isChunkInUse($x, $z)) || $this->isSpawnChunk($x, $z)){
3029 return false;
3030 }
3031
3032 $this->queueUnloadChunk($x, $z);
3033
3034 return true;
3035 }
3036
3037 public function cancelUnloadChunkRequest(int $x, int $z) : void{
3038 unset($this->unloadQueue[World::chunkHash($x, $z)]);
3039 }
3040
3041 public function unloadChunk(int $x, int $z, bool $safe = true, bool $trySave = true) : bool{
3042 if($safe && $this->isChunkInUse($x, $z)){
3043 return false;
3044 }
3045
3046 if(!$this->isChunkLoaded($x, $z)){
3047 return true;
3048 }
3049
3050 $this->timings->doChunkUnload->startTiming();
3051
3052 $chunkHash = World::chunkHash($x, $z);
3053
3054 $chunk = $this->chunks[$chunkHash] ?? null;
3055
3056 if($chunk !== null){
3057 if(ChunkUnloadEvent::hasHandlers()){
3058 $ev = new ChunkUnloadEvent($this, $x, $z, $chunk);
3059 $ev->call();
3060 if($ev->isCancelled()){
3061 $this->timings->doChunkUnload->stopTiming();
3062
3063 return false;
3064 }
3065 }
3066
3067 if($trySave && $this->getAutoSave()){
3068 $this->timings->syncChunkSave->startTiming();
3069 try{
3070 $this->provider->saveChunk($x, $z, new ChunkData(
3071 $chunk->getSubChunks(),
3072 $chunk->isPopulated(),
3073 array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk()))),
3074 array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())),
3075 ), $chunk->getTerrainDirtyFlags());
3076 }finally{
3077 $this->timings->syncChunkSave->stopTiming();
3078 }
3079 }
3080
3081 foreach($this->getChunkListeners($x, $z) as $listener){
3082 $listener->onChunkUnloaded($x, $z, $chunk);
3083 }
3084
3085 foreach($this->getChunkEntities($x, $z) as $entity){
3086 if($entity instanceof Player){
3087 continue;
3088 }
3089 $entity->close();
3090 }
3091
3092 $chunk->onUnload();
3093 }
3094
3095 unset($this->chunks[$chunkHash]);
3096 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
3097 unset($this->blockCache[$chunkHash]);
3098 unset($this->blockCollisionBoxCache[$chunkHash]);
3099 unset($this->changedBlocks[$chunkHash]);
3100 unset($this->registeredTickingChunks[$chunkHash]);
3101 $this->markTickingChunkForRecheck($x, $z);
3102
3103 if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){
3104 $this->logger->debug("Rejecting population promise for chunk $x $z");
3105 $this->chunkPopulationRequestMap[$chunkHash]->reject();
3106 unset($this->chunkPopulationRequestMap[$chunkHash]);
3107 if(isset($this->activeChunkPopulationTasks[$chunkHash])){
3108 $this->logger->debug("Marking population task for chunk $x $z as orphaned");
3109 $this->activeChunkPopulationTasks[$chunkHash] = false;
3110 }
3111 }
3112
3113 $this->timings->doChunkUnload->stopTiming();
3114
3115 return true;
3116 }
3117
3121 public function isSpawnChunk(int $X, int $Z) : bool{
3122 $spawn = $this->getSpawnLocation();
3123 $spawnX = $spawn->x >> Chunk::COORD_BIT_SIZE;
3124 $spawnZ = $spawn->z >> Chunk::COORD_BIT_SIZE;
3125
3126 return abs($X - $spawnX) <= 1 && abs($Z - $spawnZ) <= 1;
3127 }
3128
3136 public function requestSafeSpawn(?Vector3 $spawn = null) : Promise{
3138 $resolver = new PromiseResolver();
3139 $spawn ??= $this->getSpawnLocation();
3140 /*
3141 * TODO: this relies on the assumption that getSafeSpawn() will only alter the Y coordinate of the provided
3142 * position, which is currently OK, but might be a problem in the future.
3143 */
3144 $this->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
3145 function() use ($spawn, $resolver) : void{
3146 $spawn = $this->getSafeSpawn($spawn);
3147 $resolver->resolve($spawn);
3148 },
3149 function() use ($resolver) : void{
3150 $resolver->reject();
3151 }
3152 );
3153
3154 return $resolver->getPromise();
3155 }
3156
3163 public function getSafeSpawn(?Vector3 $spawn = null) : Position{
3164 if(!($spawn instanceof Vector3) || $spawn->y <= $this->minY){
3165 $spawn = $this->getSpawnLocation();
3166 }
3167
3168 $max = $this->maxY;
3169 $v = $spawn->floor();
3170 $chunk = $this->getOrLoadChunkAtPosition($v);
3171 if($chunk === null){
3172 throw new WorldException("Cannot find a safe spawn point in non-generated terrain");
3173 }
3174 $x = (int) $v->x;
3175 $z = (int) $v->z;
3176 $y = (int) min($max - 2, $v->y);
3177 $wasAir = $this->getBlockAt($x, $y - 1, $z)->getTypeId() === BlockTypeIds::AIR; //TODO: bad hack, clean up
3178 for(; $y > $this->minY; --$y){
3179 if($this->getBlockAt($x, $y, $z)->isFullCube()){
3180 if($wasAir){
3181 $y++;
3182 }
3183 break;
3184 }else{
3185 $wasAir = true;
3186 }
3187 }
3188
3189 for(; $y >= $this->minY && $y < $max; ++$y){
3190 if(!$this->getBlockAt($x, $y + 1, $z)->isFullCube()){
3191 if(!$this->getBlockAt($x, $y, $z)->isFullCube()){
3192 return new Position($spawn->x, $y === (int) $spawn->y ? $spawn->y : $y, $spawn->z, $this);
3193 }
3194 }else{
3195 ++$y;
3196 }
3197 }
3198
3199 return new Position($spawn->x, $y, $spawn->z, $this);
3200 }
3201
3205 public function getTime() : int{
3206 return $this->time;
3207 }
3208
3212 public function getTimeOfDay() : int{
3213 return $this->time % self::TIME_FULL;
3214 }
3215
3220 public function getDisplayName() : string{
3221 return $this->displayName;
3222 }
3223
3227 public function setDisplayName(string $name) : void{
3228 (new WorldDisplayNameChangeEvent($this, $this->displayName, $name))->call();
3229
3230 $this->displayName = $name;
3231 $this->provider->getWorldData()->setName($name);
3232 }
3233
3237 public function getFolderName() : string{
3238 return $this->folderName;
3239 }
3240
3244 public function setTime(int $time) : void{
3245 $this->time = $time;
3246 $this->sendTime();
3247 }
3248
3252 public function stopTime() : void{
3253 $this->stopTime = true;
3254 $this->sendTime();
3255 }
3256
3260 public function startTime() : void{
3261 $this->stopTime = false;
3262 $this->sendTime();
3263 }
3264
3268 public function getSeed() : int{
3269 return $this->provider->getWorldData()->getSeed();
3270 }
3271
3272 public function getMinY() : int{
3273 return $this->minY;
3274 }
3275
3276 public function getMaxY() : int{
3277 return $this->maxY;
3278 }
3279
3280 public function getDifficulty() : int{
3281 return $this->provider->getWorldData()->getDifficulty();
3282 }
3283
3284 public function setDifficulty(int $difficulty) : void{
3285 if($difficulty < 0 || $difficulty > 3){
3286 throw new \InvalidArgumentException("Invalid difficulty level $difficulty");
3287 }
3288 (new WorldDifficultyChangeEvent($this, $this->getDifficulty(), $difficulty))->call();
3289 $this->provider->getWorldData()->setDifficulty($difficulty);
3290
3291 foreach($this->players as $player){
3292 $player->getNetworkSession()->syncWorldDifficulty($this->getDifficulty());
3293 }
3294 }
3295
3296 private function addChunkHashToPopulationRequestQueue(int $chunkHash) : void{
3297 if(!isset($this->chunkPopulationRequestQueueIndex[$chunkHash])){
3298 $this->chunkPopulationRequestQueue->enqueue($chunkHash);
3299 $this->chunkPopulationRequestQueueIndex[$chunkHash] = true;
3300 }
3301 }
3302
3306 private function enqueuePopulationRequest(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3307 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3308 $this->addChunkHashToPopulationRequestQueue($chunkHash);
3310 $resolver = $this->chunkPopulationRequestMap[$chunkHash] = new PromiseResolver();
3311 if($associatedChunkLoader === null){
3312 $temporaryLoader = new class implements ChunkLoader{};
3313 $this->registerChunkLoader($temporaryLoader, $chunkX, $chunkZ);
3314 $resolver->getPromise()->onCompletion(
3315 fn() => $this->unregisterChunkLoader($temporaryLoader, $chunkX, $chunkZ),
3316 static function() : void{}
3317 );
3318 }
3319 return $resolver->getPromise();
3320 }
3321
3322 private function drainPopulationRequestQueue() : void{
3323 $failed = [];
3324 while(count($this->activeChunkPopulationTasks) < $this->maxConcurrentChunkPopulationTasks && !$this->chunkPopulationRequestQueue->isEmpty()){
3325 $nextChunkHash = $this->chunkPopulationRequestQueue->dequeue();
3326 unset($this->chunkPopulationRequestQueueIndex[$nextChunkHash]);
3327 World::getXZ($nextChunkHash, $nextChunkX, $nextChunkZ);
3328 if(isset($this->chunkPopulationRequestMap[$nextChunkHash])){
3329 assert(!($this->activeChunkPopulationTasks[$nextChunkHash] ?? false), "Population for chunk $nextChunkX $nextChunkZ already running");
3330 if(
3331 !$this->orderChunkPopulation($nextChunkX, $nextChunkZ, null)->isResolved() &&
3332 !isset($this->activeChunkPopulationTasks[$nextChunkHash])
3333 ){
3334 $failed[] = $nextChunkHash;
3335 }
3336 }
3337 }
3338
3339 //these requests failed even though they weren't rate limited; we can't directly re-add them to the back of the
3340 //queue because it would result in an infinite loop
3341 foreach($failed as $hash){
3342 $this->addChunkHashToPopulationRequestQueue($hash);
3343 }
3344 }
3345
3351 private function checkChunkPopulationPreconditions(int $chunkX, int $chunkZ) : array{
3352 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3353 $resolver = $this->chunkPopulationRequestMap[$chunkHash] ?? null;
3354 if($resolver !== null && isset($this->activeChunkPopulationTasks[$chunkHash])){
3355 //generation is already running
3356 return [$resolver, false];
3357 }
3358
3359 $temporaryChunkLoader = new class implements ChunkLoader{};
3360 $this->registerChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3361 $chunk = $this->loadChunk($chunkX, $chunkZ);
3362 $this->unregisterChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3363 if($chunk !== null && $chunk->isPopulated()){
3364 //chunk is already populated; return a pre-resolved promise that will directly fire callbacks assigned
3365 $resolver ??= new PromiseResolver();
3366 unset($this->chunkPopulationRequestMap[$chunkHash]);
3367 $resolver->resolve($chunk);
3368 return [$resolver, false];
3369 }
3370 return [$resolver, true];
3371 }
3372
3384 public function requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3385 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3386 if(!$proceedWithPopulation){
3387 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3388 }
3389
3390 if(count($this->activeChunkPopulationTasks) >= $this->maxConcurrentChunkPopulationTasks){
3391 //too many chunks are already generating; delay resolution of the request until later
3392 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3393 }
3394 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3395 }
3396
3407 public function orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3408 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3409 if(!$proceedWithPopulation){
3410 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3411 }
3412
3413 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3414 }
3415
3420 private function internalOrderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader, ?PromiseResolver $resolver) : Promise{
3421 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3422
3423 $timings = $this->timings->chunkPopulationOrder;
3424 $timings->startTiming();
3425
3426 try{
3427 for($xx = -1; $xx <= 1; ++$xx){
3428 for($zz = -1; $zz <= 1; ++$zz){
3429 if($this->isChunkLocked($chunkX + $xx, $chunkZ + $zz)){
3430 //chunk is already in use by another generation request; queue the request for later
3431 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3432 }
3433 }
3434 }
3435
3436 $this->activeChunkPopulationTasks[$chunkHash] = true;
3437 if($resolver === null){
3438 $resolver = new PromiseResolver();
3439 $this->chunkPopulationRequestMap[$chunkHash] = $resolver;
3440 }
3441
3442 $chunkPopulationLockId = new ChunkLockId();
3443
3444 $temporaryChunkLoader = new class implements ChunkLoader{
3445 };
3446 for($xx = -1; $xx <= 1; ++$xx){
3447 for($zz = -1; $zz <= 1; ++$zz){
3448 $this->lockChunk($chunkX + $xx, $chunkZ + $zz, $chunkPopulationLockId);
3449 $this->registerChunkLoader($temporaryChunkLoader, $chunkX + $xx, $chunkZ + $zz);
3450 }
3451 }
3452
3453 $centerChunk = $this->loadChunk($chunkX, $chunkZ);
3454 $adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
3455
3456 $this->generatorExecutor->populate(
3457 $chunkX,
3458 $chunkZ,
3459 $centerChunk,
3460 $adjacentChunks,
3461 function(Chunk $centerChunk, array $adjacentChunks) use ($chunkPopulationLockId, $chunkX, $chunkZ, $temporaryChunkLoader) : void{
3462 if(!$this->isLoaded()){
3463 return;
3464 }
3465
3466 $this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
3467 }
3468 );
3469
3470 return $resolver->getPromise();
3471 }finally{
3472 $timings->stopTiming();
3473 }
3474 }
3475
3480 private function generateChunkCallback(ChunkLockId $chunkLockId, int $x, int $z, Chunk $chunk, array $adjacentChunks, ChunkLoader $temporaryChunkLoader) : void{
3481 $timings = $this->timings->chunkPopulationCompletion;
3482 $timings->startTiming();
3483
3484 $dirtyChunks = 0;
3485 for($xx = -1; $xx <= 1; ++$xx){
3486 for($zz = -1; $zz <= 1; ++$zz){
3487 $this->unregisterChunkLoader($temporaryChunkLoader, $x + $xx, $z + $zz);
3488 if(!$this->unlockChunk($x + $xx, $z + $zz, $chunkLockId)){
3489 $dirtyChunks++;
3490 }
3491 }
3492 }
3493
3494 $index = World::chunkHash($x, $z);
3495 if(!isset($this->activeChunkPopulationTasks[$index])){
3496 throw new AssumptionFailedError("This should always be set, regardless of whether the task was orphaned or not");
3497 }
3498 if(!$this->activeChunkPopulationTasks[$index]){
3499 $this->logger->debug("Discarding orphaned population result for chunk x=$x,z=$z");
3500 unset($this->activeChunkPopulationTasks[$index]);
3501 }else{
3502 if($dirtyChunks === 0){
3503 $oldChunk = $this->loadChunk($x, $z);
3504 $this->setChunk($x, $z, $chunk);
3505
3506 foreach($adjacentChunks as $relativeChunkHash => $adjacentChunk){
3507 World::getXZ($relativeChunkHash, $relativeX, $relativeZ);
3508 if($relativeX < -1 || $relativeX > 1 || $relativeZ < -1 || $relativeZ > 1){
3509 throw new AssumptionFailedError("Adjacent chunks should be in range -1 ... +1 coordinates");
3510 }
3511 $this->setChunk($x + $relativeX, $z + $relativeZ, $adjacentChunk);
3512 }
3513
3514 if(($oldChunk === null || !$oldChunk->isPopulated()) && $chunk->isPopulated()){
3515 if(ChunkPopulateEvent::hasHandlers()){
3516 (new ChunkPopulateEvent($this, $x, $z, $chunk))->call();
3517 }
3518
3519 foreach($this->getChunkListeners($x, $z) as $listener){
3520 $listener->onChunkPopulated($x, $z, $chunk);
3521 }
3522 }
3523 }else{
3524 $this->logger->debug("Discarding population result for chunk x=$x,z=$z - terrain was modified on the main thread before async population completed");
3525 }
3526
3527 //This needs to be in this specific spot because user code might call back to orderChunkPopulation().
3528 //If it does, and finds the promise, and doesn't find an active task associated with it, it will schedule
3529 //another PopulationTask. We don't want that because we're here processing the results.
3530 //We can't remove the promise from the array before setting the chunks in the world because that would lead
3531 //to the same problem. Therefore, it's necessary that this code be split into two if/else, with this in the
3532 //middle.
3533 unset($this->activeChunkPopulationTasks[$index]);
3534
3535 if($dirtyChunks === 0){
3536 $promise = $this->chunkPopulationRequestMap[$index] ?? null;
3537 if($promise !== null){
3538 unset($this->chunkPopulationRequestMap[$index]);
3539 $promise->resolve($chunk);
3540 }else{
3541 //Handlers of ChunkPopulateEvent, ChunkLoadEvent, or just ChunkListeners can cause this
3542 $this->logger->debug("Unable to resolve population promise for chunk x=$x,z=$z - populated chunk was forcibly unloaded while setting modified chunks");
3543 }
3544 }else{
3545 //request failed, stick it back on the queue
3546 //we didn't resolve the promise or touch it in any way, so any fake chunk loaders are still valid and
3547 //don't need to be added a second time.
3548 $this->addChunkHashToPopulationRequestQueue($index);
3549 }
3550
3551 $this->drainPopulationRequestQueue();
3552 }
3553 $timings->stopTiming();
3554 }
3555
3556 public function doChunkGarbageCollection() : void{
3557 $this->timings->doChunkGC->startTiming();
3558
3559 foreach($this->chunks as $index => $chunk){
3560 if(!isset($this->unloadQueue[$index])){
3561 World::getXZ($index, $X, $Z);
3562 if(!$this->isSpawnChunk($X, $Z)){
3563 $this->unloadChunkRequest($X, $Z, true);
3564 }
3565 }
3566 $chunk->collectGarbage();
3567 }
3568
3569 $this->provider->doGarbageCollection();
3570
3571 $this->timings->doChunkGC->stopTiming();
3572 }
3573
3574 public function unloadChunks(bool $force = false) : void{
3575 if(count($this->unloadQueue) > 0){
3576 $maxUnload = 96;
3577 $now = microtime(true);
3578 foreach($this->unloadQueue as $index => $time){
3579 World::getXZ($index, $X, $Z);
3580
3581 if(!$force){
3582 if($maxUnload <= 0){
3583 break;
3584 }elseif($time > ($now - 30)){
3585 continue;
3586 }
3587 }
3588
3589 //If the chunk can't be unloaded, it stays on the queue
3590 if($this->unloadChunk($X, $Z, true)){
3591 unset($this->unloadQueue[$index]);
3592 --$maxUnload;
3593 }
3594 }
3595 }
3596 }
3597}
getBlock(?int $clickedFace=null)
Definition Item.php:494
pop(int $count=1)
Definition Item.php:433
getChunkListeners(int $chunkX, int $chunkZ)
Definition World.php:884
removeEntity(Entity $entity)
Definition World.php:2767
notifyNeighbourBlockUpdate(Vector3 $pos)
Definition World.php:1501
getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst=false)
Definition World.php:1509
getHighestAdjacentBlockLight(int $x, int $y, int $z)
Definition World.php:1888
setDisplayName(string $name)
Definition World.php:3227
getPotentialBlockSkyLightAt(int $x, int $y, int $z)
Definition World.php:1793
removeOnUnloadCallback(\Closure $callback)
Definition World.php:661
isChunkLocked(int $chunkX, int $chunkZ)
Definition World.php:2604
setSpawnLocation(Vector3 $pos)
Definition World.php:2719
getPotentialLightAt(int $x, int $y, int $z)
Definition World.php:1775
createBlockUpdatePackets(array $blocks)
Definition World.php:1084
getSafeSpawn(?Vector3 $spawn=null)
Definition World.php:3163
registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ)
Definition World.php:838
getBlockAt(int $x, int $y, int $z, bool $cached=true, bool $addToCache=true)
Definition World.php:1939
getChunkEntities(int $chunkX, int $chunkZ)
Definition World.php:2527
addEntity(Entity $entity)
Definition World.php:2733
getBlockLightAt(int $x, int $y, int $z)
Definition World.php:1818
getBlock(Vector3 $pos, bool $cached=true, bool $addToCache=true)
Definition World.php:1926
static chunkHash(int $x, int $z)
Definition World.php:378
broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet)
Definition World.php:788
getOrLoadChunkAtPosition(Vector3 $pos)
Definition World.php:2534
static chunkBlockHash(int $x, int $y, int $z)
Definition World.php:417
getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z)
Definition World.php:1873
getFullLight(Vector3 $pos)
Definition World.php:1732
isInWorld(int $x, int $y, int $z)
Definition World.php:1908
unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId)
Definition World.php:2589
getChunkLoaders(int $chunkX, int $chunkZ)
Definition World.php:771
getAdjacentChunks(int $x, int $z)
Definition World.php:2544
getChunkPlayers(int $chunkX, int $chunkZ)
Definition World.php:761
getTileAt(int $x, int $y, int $z)
Definition World.php:2484
getHighestAdjacentFullLightAt(int $x, int $y, int $z)
Definition World.php:1756
setChunkTickRadius(int $radius)
Definition World.php:1203
getViewersForPosition(Vector3 $pos)
Definition World.php:781
getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType=Entity::class, bool $includeDead=false)
Definition World.php:2427
__construct(private Server $server, string $name, private WritableWorldProvider $provider, private AsyncPool $workerPool)
Definition World.php:476
requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader)
Definition World.php:3384
addSound(Vector3 $pos, Sound $sound, ?array $players=null)
Definition World.php:689
registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ)
Definition World.php:1222
setBlock(Vector3 $pos, Block $block, bool $update=true)
Definition World.php:1994
getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity=null)
Definition World.php:2395
static getXZ(int $hash, ?int &$x, ?int &$z)
Definition World.php:443
getBlockCollisionBoxes(AxisAlignedBB $bb)
Definition World.php:1639
getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity=null)
Definition World.php:2377
isSpawnChunk(int $X, int $Z)
Definition World.php:3121
useBreakOn(Vector3 $vector, ?Item &$item=null, ?Player $player=null, bool $createParticles=false, array &$returnedItems=[])
Definition World.php:2109
getPotentialLight(Vector3 $pos)
Definition World.php:1764
addParticle(Vector3 $pos, Particle $particle, ?array $players=null)
Definition World.php:718
unregisterChunkListenerFromAll(ChunkListener $listener)
Definition World.php:871
loadChunk(int $x, int $z)
Definition World.php:2901
useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector=null, ?Player $player=null, bool $playSound=false, array &$returnedItems=[])
Definition World.php:2215
getHighestAdjacentPotentialLightAt(int $x, int $y, int $z)
Definition World.php:1783
setBlockAt(int $x, int $y, int $z, Block $block, bool $update=true)
Definition World.php:2006
unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ)
Definition World.php:1232
getRealBlockSkyLightAt(int $x, int $y, int $z)
Definition World.php:1808
static blockHash(int $x, int $y, int $z)
Definition World.php:397
getTile(Vector3 $pos)
Definition World.php:2477
getFullLightAt(int $x, int $y, int $z)
Definition World.php:1743
getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z)
Definition World.php:1881
orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader)
Definition World.php:3407
dropExperience(Vector3 $pos, int $amount)
Definition World.php:2086
isInLoadedTerrain(Vector3 $pos)
Definition World.php:2692
lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId)
Definition World.php:2572
getHighestBlockAt(int $x, int $z)
Definition World.php:2682
scheduleDelayedBlockUpdate(Vector3 $pos, int $delay)
Definition World.php:1460
static getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z)
Definition World.php:427
addOnUnloadCallback(\Closure $callback)
Definition World.php:656
requestSafeSpawn(?Vector3 $spawn=null)
Definition World.php:3136
unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ)
Definition World.php:855
getTile(int $x, int $y, int $z)
Definition Chunk.php:229
getHighestBlockAt(int $x, int $z)
Definition Chunk.php:121
getBlockStateId(int $x, int $y, int $z)
Definition Chunk.php:101