PocketMine-MP 5.35.1 git-e32e836dad793a3a3c8ddd8927c00e112b1e576a
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
62use pocketmine\item\ItemUseResult;
95use pocketmine\world\format\LightArray;
112use function abs;
113use function array_filter;
114use function array_key_exists;
115use function array_keys;
116use function array_map;
117use function array_merge;
118use function array_sum;
119use function array_values;
120use function assert;
121use function cos;
122use function count;
123use function floor;
124use function get_class;
125use function gettype;
126use function is_a;
127use function is_object;
128use function max;
129use function microtime;
130use function min;
131use function morton2d_decode;
132use function morton2d_encode;
133use function morton3d_decode;
134use function morton3d_encode;
135use function mt_rand;
136use function preg_match;
137use function spl_object_id;
138use function strtolower;
139use function trim;
140use const M_PI;
141use const PHP_INT_MAX;
142use const PHP_INT_MIN;
143
144#include <rules/World.h>
145
151class World implements ChunkManager{
152
153 private static int $worldIdCounter = 1;
154
155 public const Y_MAX = 320;
156 public const Y_MIN = -64;
157
158 public const TIME_DAY = 1000;
159 public const TIME_NOON = 6000;
160 public const TIME_SUNSET = 12000;
161 public const TIME_NIGHT = 13000;
162 public const TIME_MIDNIGHT = 18000;
163 public const TIME_SUNRISE = 23000;
164
165 public const TIME_FULL = 24000;
166
167 public const DIFFICULTY_PEACEFUL = 0;
168 public const DIFFICULTY_EASY = 1;
169 public const DIFFICULTY_NORMAL = 2;
170 public const DIFFICULTY_HARD = 3;
171
172 public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3;
173
174 //TODO: this could probably do with being a lot bigger
175 private const BLOCK_CACHE_SIZE_CAP = 2048;
176
181 private array $players = [];
182
187 private array $entities = [];
192 private array $entityLastKnownPositions = [];
193
198 private array $entitiesByChunk = [];
199
204 public array $updateEntities = [];
205
206 private bool $inDynamicStateRecalculation = false;
211 private array $blockCache = [];
212 private int $blockCacheSize = 0;
217 private array $blockCollisionBoxCache = [];
218
219 private int $sendTimeTicker = 0;
220
221 private int $worldId;
222
223 private int $providerGarbageCollectionTicker = 0;
224
225 private int $minY;
226 private int $maxY;
227
232 private array $registeredTickingChunks = [];
233
240 private array $validTickingChunks = [];
241
247 private array $recheckTickingChunks = [];
248
253 private array $chunkLoaders = [];
254
259 private array $chunkListeners = [];
264 private array $playerChunkListeners = [];
265
270 private array $packetBuffersByChunk = [];
271
276 private array $unloadQueue = [];
277
278 private int $time;
279 public bool $stopTime = false;
280
281 private float $sunAnglePercentage = 0.0;
282 private int $skyLightReduction = 0;
283
284 private string $displayName;
285
290 private array $chunks = [];
291
296 private array $knownUngeneratedChunks = [];
297
302 private array $changedBlocks = [];
303
305 private ReversePriorityQueue $scheduledBlockUpdateQueue;
310 private array $scheduledBlockUpdateQueueIndex = [];
311
313 private \SplQueue $neighbourBlockUpdateQueue;
318 private array $neighbourBlockUpdateQueueIndex = [];
319
324 private array $activeChunkPopulationTasks = [];
329 private array $chunkLock = [];
330 private int $maxConcurrentChunkPopulationTasks = 2;
335 private array $chunkPopulationRequestMap = [];
340 private \SplQueue $chunkPopulationRequestQueue;
345 private array $chunkPopulationRequestQueueIndex = [];
346
347 private readonly GeneratorExecutor $generatorExecutor;
348
349 private bool $autoSave = true;
350
351 private int $sleepTicks = 0;
352
353 private int $chunkTickRadius;
354 private int $tickedBlocksPerSubchunkPerTick = self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK;
359 private array $randomTickBlocks = [];
360
361 public WorldTimings $timings;
362
363 public float $tickRateTime = 0;
364
365 private bool $doingTick = false;
366
367 private bool $unloaded = false;
372 private array $unloadCallbacks = [];
373
374 private ?BlockLightUpdate $blockLightUpdate = null;
375 private ?SkyLightUpdate $skyLightUpdate = null;
376
377 private \Logger $logger;
378
379 private RuntimeBlockStateRegistry $blockStateRegistry;
380
384 public static function chunkHash(int $x, int $z) : int{
385 return morton2d_encode($x, $z);
386 }
387
388 private const MORTON3D_BIT_SIZE = 21;
389 private const BLOCKHASH_Y_BITS = 9;
390 private const BLOCKHASH_Y_PADDING = 64; //size (in blocks) of padding after both boundaries of the Y axis
391 private const BLOCKHASH_Y_OFFSET = self::BLOCKHASH_Y_PADDING - self::Y_MIN;
392 private const BLOCKHASH_Y_MASK = (1 << self::BLOCKHASH_Y_BITS) - 1;
393 private const BLOCKHASH_XZ_MASK = (1 << self::MORTON3D_BIT_SIZE) - 1;
394 private const BLOCKHASH_XZ_EXTRA_BITS = (self::MORTON3D_BIT_SIZE - self::BLOCKHASH_Y_BITS) >> 1;
395 private const BLOCKHASH_XZ_EXTRA_MASK = (1 << self::BLOCKHASH_XZ_EXTRA_BITS) - 1;
396 private const BLOCKHASH_XZ_SIGN_SHIFT = 64 - self::MORTON3D_BIT_SIZE - self::BLOCKHASH_XZ_EXTRA_BITS;
397 private const BLOCKHASH_X_SHIFT = self::BLOCKHASH_Y_BITS;
398 private const BLOCKHASH_Z_SHIFT = self::BLOCKHASH_X_SHIFT + self::BLOCKHASH_XZ_EXTRA_BITS;
399
403 public static function blockHash(int $x, int $y, int $z) : int{
404 $shiftedY = $y + self::BLOCKHASH_Y_OFFSET;
405 if(($shiftedY & (~0 << self::BLOCKHASH_Y_BITS)) !== 0){
406 throw new \InvalidArgumentException("Y coordinate $y is out of range!");
407 }
408 //morton3d gives us 21 bits on each axis, but the Y axis only requires 9
409 //so we use the extra space on Y (12 bits) and add 6 extra bits from X and Z instead.
410 //if we ever need more space for Y (e.g. due to expansion), take bits from X/Z to compensate.
411 return morton3d_encode(
412 $x & self::BLOCKHASH_XZ_MASK,
413 ($shiftedY /* & self::BLOCKHASH_Y_MASK */) |
414 ((($x >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_X_SHIFT) |
415 ((($z >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_Z_SHIFT),
416 $z & self::BLOCKHASH_XZ_MASK
417 );
418 }
419
423 public static function chunkBlockHash(int $x, int $y, int $z) : int{
424 return morton3d_encode($x, $y, $z);
425 }
426
433 public static function getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z) : void{
434 [$baseX, $baseY, $baseZ] = morton3d_decode($hash);
435
436 $extraX = ((($baseY >> self::BLOCKHASH_X_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
437 $extraZ = ((($baseY >> self::BLOCKHASH_Z_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
438
439 $x = (($baseX & self::BLOCKHASH_XZ_MASK) | $extraX) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
440 $y = ($baseY & self::BLOCKHASH_Y_MASK) - self::BLOCKHASH_Y_OFFSET;
441 $z = (($baseZ & self::BLOCKHASH_XZ_MASK) | $extraZ) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
442 }
443
449 public static function getXZ(int $hash, ?int &$x, ?int &$z) : void{
450 [$x, $z] = morton2d_decode($hash);
451 }
452
453 public static function getDifficultyFromString(string $str) : int{
454 switch(strtolower(trim($str))){
455 case "0":
456 case "peaceful":
457 case "p":
458 return World::DIFFICULTY_PEACEFUL;
459
460 case "1":
461 case "easy":
462 case "e":
463 return World::DIFFICULTY_EASY;
464
465 case "2":
466 case "normal":
467 case "n":
468 return World::DIFFICULTY_NORMAL;
469
470 case "3":
471 case "hard":
472 case "h":
473 return World::DIFFICULTY_HARD;
474 }
475
476 return -1;
477 }
478
482 public function __construct(
483 private Server $server,
484 private string $folderName,
485 private WritableWorldProvider $provider,
486 private AsyncPool $workerPool
487 ){
488 $this->worldId = self::$worldIdCounter++;
489
490 $this->displayName = $this->provider->getWorldData()->getName();
491 $this->logger = new \PrefixedLogger($server->getLogger(), "World: $this->displayName");
492
493 $this->blockStateRegistry = RuntimeBlockStateRegistry::getInstance();
494 $this->minY = $this->provider->getWorldMinY();
495 $this->maxY = $this->provider->getWorldMaxY();
496
497 $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_preparing($this->displayName)));
498 $generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
499 throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
500 $generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
501
502 $executorSetupParameters = new GeneratorExecutorSetupParameters(
503 worldMinY: $this->minY,
504 worldMaxY: $this->maxY,
505 generatorSeed: $this->getSeed(),
506 generatorClass: $generator->getGeneratorClass(),
507 generatorSettings: $this->provider->getWorldData()->getGeneratorOptions()
508 );
509 $this->generatorExecutor = $generator->isFast() ?
510 new SyncGeneratorExecutor($executorSetupParameters) :
512 $this->logger,
513 $this->workerPool,
514 $executorSetupParameters,
515 $this->worldId
516 );
517
518 $this->chunkPopulationRequestQueue = new \SplQueue();
519 $this->addOnUnloadCallback(function() : void{
520 $this->logger->debug("Cancelling unfulfilled generation requests");
521
522 foreach($this->chunkPopulationRequestMap as $chunkHash => $promise){
523 $promise->reject();
524 unset($this->chunkPopulationRequestMap[$chunkHash]);
525 }
526 if(count($this->chunkPopulationRequestMap) !== 0){
527 //TODO: this might actually get hit because generation rejection callbacks might try to schedule new
528 //requests, and we can't prevent that right now because there's no way to detect "unloading" state
529 throw new AssumptionFailedError("New generation requests scheduled during unload");
530 }
531 });
532
533 $this->scheduledBlockUpdateQueue = new ReversePriorityQueue();
534 $this->scheduledBlockUpdateQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
535
536 $this->neighbourBlockUpdateQueue = new \SplQueue();
537
538 $this->time = $this->provider->getWorldData()->getTime();
539
540 $cfg = $this->server->getConfigGroup();
541 $this->chunkTickRadius = min($this->server->getViewDistance(), max(0, $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_TICK_RADIUS, 4)));
542 if($cfg->getPropertyInt("chunk-ticking.per-tick", 40) <= 0){
543 //TODO: this needs l10n
544 $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.");
545 $this->chunkTickRadius = 0;
546 }
547 $this->tickedBlocksPerSubchunkPerTick = $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_BLOCKS_PER_SUBCHUNK_PER_TICK, self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK);
548 $this->maxConcurrentChunkPopulationTasks = $cfg->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2);
549
550 $this->initRandomTickBlocksFromConfig($cfg);
551
552 $this->timings = new WorldTimings($this);
553 }
554
555 private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{
556 $dontTickBlocks = [];
557 $parser = StringToItemParser::getInstance();
558 foreach($cfg->getProperty(YmlServerProperties::CHUNK_TICKING_DISABLE_BLOCK_TICKING, []) as $name){
559 $name = (string) $name;
560 $item = $parser->parse($name);
561 if($item !== null){
562 $block = $item->getBlock();
563 }elseif(preg_match("/^-?\d+$/", $name) === 1){
564 //TODO: this is a really sketchy hack - remove this as soon as possible
565 try{
566 $blockStateData = GlobalBlockStateHandlers::getUpgrader()->upgradeIntIdMeta((int) $name, 0);
567 }catch(BlockStateDeserializeException){
568 continue;
569 }
570 $block = $this->blockStateRegistry->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData));
571 }else{
572 //TODO: we probably ought to log an error here
573 continue;
574 }
575
576 if($block->getTypeId() !== BlockTypeIds::AIR){
577 $dontTickBlocks[$block->getTypeId()] = $name;
578 }
579 }
580
581 foreach($this->blockStateRegistry->getAllKnownStates() as $state){
582 $dontTickName = $dontTickBlocks[$state->getTypeId()] ?? null;
583 if($dontTickName === null && $state->ticksRandomly()){
584 $this->randomTickBlocks[$state->getStateId()] = true;
585 }
586 }
587 }
588
589 public function getTickRateTime() : float{
590 return $this->tickRateTime;
591 }
592
593 public function getServer() : Server{
594 return $this->server;
595 }
596
597 public function getLogger() : \Logger{
598 return $this->logger;
599 }
600
601 final public function getProvider() : WritableWorldProvider{
602 return $this->provider;
603 }
604
608 final public function getId() : int{
609 return $this->worldId;
610 }
611
612 public function isLoaded() : bool{
613 return !$this->unloaded;
614 }
615
619 public function onUnload() : void{
620 if($this->unloaded){
621 throw new \LogicException("Tried to close a world which is already closed");
622 }
623
624 foreach($this->unloadCallbacks as $callback){
625 $callback();
626 }
627 $this->unloadCallbacks = [];
628
629 foreach($this->chunks as $chunkHash => $chunk){
630 self::getXZ($chunkHash, $chunkX, $chunkZ);
631 $this->unloadChunk($chunkX, $chunkZ, false);
632 }
633 $this->knownUngeneratedChunks = [];
634 foreach($this->entitiesByChunk as $chunkHash => $entities){
635 self::getXZ($chunkHash, $chunkX, $chunkZ);
636
637 $leakedEntities = 0;
638 foreach($entities as $entity){
639 if(!$entity->isFlaggedForDespawn()){
640 $leakedEntities++;
641 }
642 $entity->close();
643 }
644 if($leakedEntities !== 0){
645 $this->logger->warning("$leakedEntities leaked entities found in ungenerated chunk $chunkX $chunkZ during unload, they won't be saved!");
646 }
647 }
648
649 $this->save();
650
651 $this->generatorExecutor->shutdown();
652
653 $this->provider->close();
654 $this->blockCache = [];
655 $this->blockCacheSize = 0;
656 $this->blockCollisionBoxCache = [];
657
658 $this->unloaded = true;
659 }
660
662 public function addOnUnloadCallback(\Closure $callback) : void{
663 $this->unloadCallbacks[spl_object_id($callback)] = $callback;
664 }
665
667 public function removeOnUnloadCallback(\Closure $callback) : void{
668 unset($this->unloadCallbacks[spl_object_id($callback)]);
669 }
670
679 private function filterViewersForPosition(Vector3 $pos, array $allowed) : array{
680 $candidates = $this->getViewersForPosition($pos);
681 $filtered = [];
682 foreach($allowed as $player){
683 $k = spl_object_id($player);
684 if(isset($candidates[$k])){
685 $filtered[$k] = $candidates[$k];
686 }
687 }
688
689 return $filtered;
690 }
691
695 public function addSound(Vector3 $pos, Sound $sound, ?array $players = null) : void{
696 $players ??= $this->getViewersForPosition($pos);
697
698 if(WorldSoundEvent::hasHandlers()){
699 $ev = new WorldSoundEvent($this, $sound, $pos, $players);
700 $ev->call();
701 if($ev->isCancelled()){
702 return;
703 }
704
705 $sound = $ev->getSound();
706 $players = $ev->getRecipients();
707 }
708
709 $pk = $sound->encode($pos);
710 if(count($pk) > 0){
711 if($players === $this->getViewersForPosition($pos)){
712 foreach($pk as $e){
713 $this->broadcastPacketToViewers($pos, $e);
714 }
715 }else{
716 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
717 }
718 }
719 }
720
724 public function addParticle(Vector3 $pos, Particle $particle, ?array $players = null) : void{
725 $players ??= $this->getViewersForPosition($pos);
726
727 if(WorldParticleEvent::hasHandlers()){
728 $ev = new WorldParticleEvent($this, $particle, $pos, $players);
729 $ev->call();
730 if($ev->isCancelled()){
731 return;
732 }
733
734 $particle = $ev->getParticle();
735 $players = $ev->getRecipients();
736 }
737
738 $pk = $particle->encode($pos);
739 if(count($pk) > 0){
740 if($players === $this->getViewersForPosition($pos)){
741 foreach($pk as $e){
742 $this->broadcastPacketToViewers($pos, $e);
743 }
744 }else{
745 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
746 }
747 }
748 }
749
750 public function getAutoSave() : bool{
751 return $this->autoSave;
752 }
753
754 public function setAutoSave(bool $value) : void{
755 $this->autoSave = $value;
756 }
757
767 public function getChunkPlayers(int $chunkX, int $chunkZ) : array{
768 return $this->playerChunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? [];
769 }
770
777 public function getChunkLoaders(int $chunkX, int $chunkZ) : array{
778 return $this->chunkLoaders[World::chunkHash($chunkX, $chunkZ)] ?? [];
779 }
780
787 public function getViewersForPosition(Vector3 $pos) : array{
788 return $this->getChunkPlayers($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
789 }
790
794 public function broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet) : void{
795 $this->broadcastPacketToPlayersUsingChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE, $packet);
796 }
797
798 private function broadcastPacketToPlayersUsingChunk(int $chunkX, int $chunkZ, ClientboundPacket $packet) : void{
799 if(!isset($this->packetBuffersByChunk[$index = World::chunkHash($chunkX, $chunkZ)])){
800 $this->packetBuffersByChunk[$index] = [$packet];
801 }else{
802 $this->packetBuffersByChunk[$index][] = $packet;
803 }
804 }
805
806 public function registerChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ, bool $autoLoad = true) : void{
807 $loaderId = spl_object_id($loader);
808
809 if(!isset($this->chunkLoaders[$chunkHash = World::chunkHash($chunkX, $chunkZ)])){
810 $this->chunkLoaders[$chunkHash] = [];
811 }elseif(isset($this->chunkLoaders[$chunkHash][$loaderId])){
812 return;
813 }
814
815 $this->chunkLoaders[$chunkHash][$loaderId] = $loader;
816
817 $this->cancelUnloadChunkRequest($chunkX, $chunkZ);
818
819 if($autoLoad){
820 $this->loadChunk($chunkX, $chunkZ);
821 }
822 }
823
824 public function unregisterChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ) : void{
825 $chunkHash = World::chunkHash($chunkX, $chunkZ);
826 $loaderId = spl_object_id($loader);
827 if(isset($this->chunkLoaders[$chunkHash][$loaderId])){
828 if(count($this->chunkLoaders[$chunkHash]) === 1){
829 unset($this->chunkLoaders[$chunkHash]);
830 $this->unloadChunkRequest($chunkX, $chunkZ, true);
831 if(isset($this->chunkPopulationRequestMap[$chunkHash]) && !isset($this->activeChunkPopulationTasks[$chunkHash])){
832 $this->chunkPopulationRequestMap[$chunkHash]->reject();
833 unset($this->chunkPopulationRequestMap[$chunkHash]);
834 }
835 }else{
836 unset($this->chunkLoaders[$chunkHash][$loaderId]);
837 }
838 }
839 }
840
844 public function registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{
845 $hash = World::chunkHash($chunkX, $chunkZ);
846 if(isset($this->chunkListeners[$hash])){
847 $this->chunkListeners[$hash][spl_object_id($listener)] = $listener;
848 }else{
849 $this->chunkListeners[$hash] = [spl_object_id($listener) => $listener];
850 }
851 if($listener instanceof Player){
852 $this->playerChunkListeners[$hash][spl_object_id($listener)] = $listener;
853 }
854 }
855
861 public function unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{
862 $hash = World::chunkHash($chunkX, $chunkZ);
863 if(isset($this->chunkListeners[$hash])){
864 if(count($this->chunkListeners[$hash]) === 1){
865 unset($this->chunkListeners[$hash]);
866 unset($this->playerChunkListeners[$hash]);
867 }else{
868 unset($this->chunkListeners[$hash][spl_object_id($listener)]);
869 unset($this->playerChunkListeners[$hash][spl_object_id($listener)]);
870 }
871 }
872 }
873
877 public function unregisterChunkListenerFromAll(ChunkListener $listener) : void{
878 foreach($this->chunkListeners as $hash => $listeners){
879 World::getXZ($hash, $chunkX, $chunkZ);
880 $this->unregisterChunkListener($listener, $chunkX, $chunkZ);
881 }
882 }
883
890 public function getChunkListeners(int $chunkX, int $chunkZ) : array{
891 return $this->chunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? [];
892 }
893
897 public function sendTime(Player ...$targets) : void{
898 if(count($targets) === 0){
899 $targets = $this->players;
900 }
901 foreach($targets as $player){
902 $player->getNetworkSession()->syncWorldTime($this->time);
903 }
904 }
905
906 public function isDoingTick() : bool{
907 return $this->doingTick;
908 }
909
913 public function doTick(int $currentTick) : void{
914 if($this->unloaded){
915 throw new \LogicException("Attempted to tick a world which has been closed");
916 }
917
918 $this->timings->doTick->startTiming();
919 $this->doingTick = true;
920 try{
921 $this->actuallyDoTick($currentTick);
922 }finally{
923 $this->doingTick = false;
924 $this->timings->doTick->stopTiming();
925 }
926 }
927
928 protected function actuallyDoTick(int $currentTick) : void{
929 if(!$this->stopTime){
930 //this simulates an overflow, as would happen in any language which doesn't do stupid things to var types
931 if($this->time === PHP_INT_MAX){
932 $this->time = PHP_INT_MIN;
933 }else{
934 $this->time++;
935 }
936 }
937
938 $this->sunAnglePercentage = $this->computeSunAnglePercentage(); //Sun angle depends on the current time
939 $this->skyLightReduction = $this->computeSkyLightReduction(); //Sky light reduction depends on the sun angle
940
941 if(++$this->sendTimeTicker === 200){
942 $this->sendTime();
943 $this->sendTimeTicker = 0;
944 }
945
946 $this->unloadChunks();
947 if(++$this->providerGarbageCollectionTicker >= 6000){
948 $this->provider->doGarbageCollection();
949 $this->providerGarbageCollectionTicker = 0;
950 }
951
952 $this->timings->scheduledBlockUpdates->startTiming();
953 //Delayed updates
954 while($this->scheduledBlockUpdateQueue->count() > 0 && $this->scheduledBlockUpdateQueue->current()["priority"] <= $currentTick){
956 $vec = $this->scheduledBlockUpdateQueue->extract()["data"];
957 unset($this->scheduledBlockUpdateQueueIndex[World::blockHash($vec->x, $vec->y, $vec->z)]);
958 if(!$this->isInLoadedTerrain($vec)){
959 continue;
960 }
961 $block = $this->getBlock($vec);
962 $block->onScheduledUpdate();
963 }
964 $this->timings->scheduledBlockUpdates->stopTiming();
965
966 $this->timings->neighbourBlockUpdates->startTiming();
967 //Normal updates
968 while($this->neighbourBlockUpdateQueue->count() > 0){
969 $index = $this->neighbourBlockUpdateQueue->dequeue();
970 unset($this->neighbourBlockUpdateQueueIndex[$index]);
971 World::getBlockXYZ($index, $x, $y, $z);
972 if(!$this->isChunkLoaded($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)){
973 continue;
974 }
975
976 $block = $this->getBlockAt($x, $y, $z);
977
978 if(BlockUpdateEvent::hasHandlers()){
979 $ev = new BlockUpdateEvent($block);
980 $ev->call();
981 if($ev->isCancelled()){
982 continue;
983 }
984 }
985 foreach($this->getNearbyEntities(AxisAlignedBB::one()->offsetCopy($x, $y, $z)) as $entity){
986 $entity->onNearbyBlockChange();
987 }
988 $block->onNearbyBlockChange();
989 }
990
991 $this->timings->neighbourBlockUpdates->stopTiming();
992
993 $this->timings->entityTick->startTiming();
994 //Update entities that need update
995 foreach($this->updateEntities as $id => $entity){
996 if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->onUpdate($currentTick)){
997 unset($this->updateEntities[$id]);
998 }
999 if($entity->isFlaggedForDespawn()){
1000 $entity->close();
1001 }
1002 }
1003 $this->timings->entityTick->stopTiming();
1004
1005 $this->timings->randomChunkUpdates->startTiming();
1006 $this->tickChunks();
1007 $this->timings->randomChunkUpdates->stopTiming();
1008
1009 $this->executeQueuedLightUpdates();
1010
1011 if(count($this->changedBlocks) > 0){
1012 if(count($this->players) > 0){
1013 foreach($this->changedBlocks as $index => $blocks){
1014 if(count($blocks) === 0){ //blocks can be set normally and then later re-set with direct send
1015 continue;
1016 }
1017 World::getXZ($index, $chunkX, $chunkZ);
1018 if(!$this->isChunkLoaded($chunkX, $chunkZ)){
1019 //a previous chunk may have caused this one to be unloaded by a ChunkListener
1020 continue;
1021 }
1022 if(count($blocks) > 512){
1023 $chunk = $this->getChunk($chunkX, $chunkZ) ?? throw new AssumptionFailedError("We already checked that the chunk is loaded");
1024 foreach($this->getChunkPlayers($chunkX, $chunkZ) as $p){
1025 $p->onChunkChanged($chunkX, $chunkZ, $chunk);
1026 }
1027 }else{
1028 foreach($this->createBlockUpdatePackets($blocks) as $packet){
1029 $this->broadcastPacketToPlayersUsingChunk($chunkX, $chunkZ, $packet);
1030 }
1031 }
1032 }
1033 }
1034
1035 $this->changedBlocks = [];
1036
1037 }
1038
1039 if($this->sleepTicks > 0 && --$this->sleepTicks <= 0){
1040 $this->checkSleep();
1041 }
1042
1043 foreach($this->packetBuffersByChunk as $index => $entries){
1044 World::getXZ($index, $chunkX, $chunkZ);
1045 $chunkPlayers = $this->getChunkPlayers($chunkX, $chunkZ);
1046 if(count($chunkPlayers) > 0){
1047 NetworkBroadcastUtils::broadcastPackets($chunkPlayers, $entries);
1048 }
1049 }
1050
1051 $this->packetBuffersByChunk = [];
1052 }
1053
1054 public function checkSleep() : void{
1055 if(count($this->players) === 0){
1056 return;
1057 }
1058
1059 $resetTime = true;
1060 foreach($this->getPlayers() as $p){
1061 if(!$p->isSleeping()){
1062 $resetTime = false;
1063 break;
1064 }
1065 }
1066
1067 if($resetTime){
1068 $time = $this->getTimeOfDay();
1069
1070 if($time >= World::TIME_NIGHT && $time < World::TIME_SUNRISE){
1071 $this->setTime($this->getTime() + World::TIME_FULL - $time);
1072
1073 foreach($this->getPlayers() as $p){
1074 $p->stopSleep();
1075 }
1076 }
1077 }
1078 }
1079
1080 public function setSleepTicks(int $ticks) : void{
1081 $this->sleepTicks = $ticks;
1082 }
1083
1090 public function createBlockUpdatePackets(array $blocks) : array{
1091 $packets = [];
1092
1093 $blockTranslator = TypeConverter::getInstance()->getBlockTranslator();
1094
1095 foreach($blocks as $b){
1096 if(!($b instanceof Vector3)){
1097 throw new \TypeError("Expected Vector3 in blocks array, got " . (is_object($b) ? get_class($b) : gettype($b)));
1098 }
1099
1100 $fullBlock = $this->getBlockAt($b->x, $b->y, $b->z);
1101 $blockPosition = BlockPosition::fromVector3($b);
1102
1103 $tile = $this->getTileAt($b->x, $b->y, $b->z);
1104 if($tile instanceof Spawnable){
1105 $expectedClass = $fullBlock->getIdInfo()->getTileClass();
1106 if($expectedClass !== null && $tile instanceof $expectedClass && count($fakeStateProperties = $tile->getRenderUpdateBugWorkaroundStateProperties($fullBlock)) > 0){
1107 $originalStateData = $blockTranslator->internalIdToNetworkStateData($fullBlock->getStateId());
1108 $fakeStateData = new BlockStateData(
1109 $originalStateData->getName(),
1110 array_merge($originalStateData->getStates(), $fakeStateProperties),
1111 $originalStateData->getVersion()
1112 );
1113 $packets[] = UpdateBlockPacket::create(
1114 $blockPosition,
1115 $blockTranslator->getBlockStateDictionary()->lookupStateIdFromData($fakeStateData) ?? throw new AssumptionFailedError("Unmapped fake blockstate data: " . $fakeStateData->toNbt()),
1116 UpdateBlockPacket::FLAG_NETWORK,
1117 UpdateBlockPacket::DATA_LAYER_NORMAL
1118 );
1119 }
1120 }
1121 $packets[] = UpdateBlockPacket::create(
1122 $blockPosition,
1123 $blockTranslator->internalIdToNetworkId($fullBlock->getStateId()),
1124 UpdateBlockPacket::FLAG_NETWORK,
1125 UpdateBlockPacket::DATA_LAYER_NORMAL
1126 );
1127
1128 if($tile instanceof Spawnable){
1129 $packets[] = BlockActorDataPacket::create($blockPosition, $tile->getSerializedSpawnCompound());
1130 }
1131 }
1132
1133 return $packets;
1134 }
1135
1136 public function clearCache(bool $force = false) : void{
1137 if($force){
1138 $this->blockCache = [];
1139 $this->blockCacheSize = 0;
1140 $this->blockCollisionBoxCache = [];
1141 }else{
1142 //Recalculate this when we're asked - blockCacheSize may be higher than the real size
1143 $this->blockCacheSize = 0;
1144 foreach($this->blockCache as $list){
1145 $this->blockCacheSize += count($list);
1146 if($this->blockCacheSize > self::BLOCK_CACHE_SIZE_CAP){
1147 $this->blockCache = [];
1148 $this->blockCacheSize = 0;
1149 break;
1150 }
1151 }
1152
1153 $count = 0;
1154 foreach($this->blockCollisionBoxCache as $list){
1155 $count += count($list);
1156 if($count > self::BLOCK_CACHE_SIZE_CAP){
1157 //TODO: Is this really the best logic?
1158 $this->blockCollisionBoxCache = [];
1159 break;
1160 }
1161 }
1162 }
1163 }
1164
1165 private function trimBlockCache() : void{
1166 $before = $this->blockCacheSize;
1167 //Since PHP maintains key order, earliest in foreach should be the oldest entries
1168 //Older entries are less likely to be hot, so destroying these should usually have the lowest impact on performance
1169 foreach($this->blockCache as $chunkHash => $blocks){
1170 unset($this->blockCache[$chunkHash]);
1171 $this->blockCacheSize -= count($blocks);
1172 if($this->blockCacheSize < self::BLOCK_CACHE_SIZE_CAP){
1173 break;
1174 }
1175 }
1176 }
1177
1182 public function getRandomTickedBlocks() : array{
1183 return $this->randomTickBlocks;
1184 }
1185
1186 public function addRandomTickedBlock(Block $block) : void{
1187 if($block instanceof UnknownBlock){
1188 throw new \InvalidArgumentException("Cannot do random-tick on unknown block");
1189 }
1190 $this->randomTickBlocks[$block->getStateId()] = true;
1191 }
1192
1193 public function removeRandomTickedBlock(Block $block) : void{
1194 unset($this->randomTickBlocks[$block->getStateId()]);
1195 }
1196
1201 public function getChunkTickRadius() : int{
1202 return $this->chunkTickRadius;
1203 }
1204
1209 public function setChunkTickRadius(int $radius) : void{
1210 $this->chunkTickRadius = $radius;
1211 }
1212
1220 public function getTickingChunks() : array{
1221 return array_keys($this->validTickingChunks);
1222 }
1223
1228 public function registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
1229 $chunkPosHash = World::chunkHash($chunkX, $chunkZ);
1230 $this->registeredTickingChunks[$chunkPosHash][spl_object_id($ticker)] = $ticker;
1231 $this->recheckTickingChunks[$chunkPosHash] = $chunkPosHash;
1232 }
1233
1238 public function unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
1239 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1240 $tickerId = spl_object_id($ticker);
1241 if(isset($this->registeredTickingChunks[$chunkHash][$tickerId])){
1242 if(count($this->registeredTickingChunks[$chunkHash]) === 1){
1243 unset(
1244 $this->registeredTickingChunks[$chunkHash],
1245 $this->recheckTickingChunks[$chunkHash],
1246 $this->validTickingChunks[$chunkHash]
1247 );
1248 }else{
1249 unset($this->registeredTickingChunks[$chunkHash][$tickerId]);
1250 }
1251 }
1252 }
1253
1254 private function tickChunks() : void{
1255 if($this->chunkTickRadius <= 0 || count($this->registeredTickingChunks) === 0){
1256 return;
1257 }
1258
1259 if(count($this->recheckTickingChunks) > 0){
1260 $this->timings->randomChunkUpdatesChunkSelection->startTiming();
1261
1262 $chunkTickableCache = [];
1263
1264 foreach($this->recheckTickingChunks as $hash => $_){
1265 World::getXZ($hash, $chunkX, $chunkZ);
1266 if($this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){
1267 $this->validTickingChunks[$hash] = $hash;
1268 }
1269 }
1270 $this->recheckTickingChunks = [];
1271
1272 $this->timings->randomChunkUpdatesChunkSelection->stopTiming();
1273 }
1274
1275 foreach($this->validTickingChunks as $index => $_){
1276 World::getXZ($index, $chunkX, $chunkZ);
1277
1278 $this->tickChunk($chunkX, $chunkZ);
1279 }
1280 }
1281
1288 private function isChunkTickable(int $chunkX, int $chunkZ, array &$cache) : bool{
1289 for($cx = -1; $cx <= 1; ++$cx){
1290 for($cz = -1; $cz <= 1; ++$cz){
1291 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1292 if(isset($cache[$chunkHash])){
1293 if(!$cache[$chunkHash]){
1294 return false;
1295 }
1296 continue;
1297 }
1298 if($this->isChunkLocked($chunkX + $cx, $chunkZ + $cz)){
1299 $cache[$chunkHash] = false;
1300 return false;
1301 }
1302 $adjacentChunk = $this->getChunk($chunkX + $cx, $chunkZ + $cz);
1303 if($adjacentChunk === null || !$adjacentChunk->isPopulated()){
1304 $cache[$chunkHash] = false;
1305 return false;
1306 }
1307 $lightPopulatedState = $adjacentChunk->isLightPopulated();
1308 if($lightPopulatedState !== true){
1309 if($lightPopulatedState === false){
1310 $this->orderLightPopulation($chunkX + $cx, $chunkZ + $cz);
1311 }
1312 $cache[$chunkHash] = false;
1313 return false;
1314 }
1315
1316 $cache[$chunkHash] = true;
1317 }
1318 }
1319
1320 return true;
1321 }
1322
1332 private function markTickingChunkForRecheck(int $chunkX, int $chunkZ) : void{
1333 for($cx = -1; $cx <= 1; ++$cx){
1334 for($cz = -1; $cz <= 1; ++$cz){
1335 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1336 unset($this->validTickingChunks[$chunkHash]);
1337 if(isset($this->registeredTickingChunks[$chunkHash])){
1338 $this->recheckTickingChunks[$chunkHash] = $chunkHash;
1339 }else{
1340 unset($this->recheckTickingChunks[$chunkHash]);
1341 }
1342 }
1343 }
1344 }
1345
1346 private function orderLightPopulation(int $chunkX, int $chunkZ) : void{
1347 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1348 $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated();
1349 if($lightPopulatedState === false){
1350 $this->chunks[$chunkHash]->setLightPopulated(null);
1351 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1352
1353 $this->workerPool->submitTask(new LightPopulationTask(
1354 $this->chunks[$chunkHash],
1355 function(array $blockLight, array $skyLight, array $heightMap) use ($chunkX, $chunkZ) : void{
1362 if($this->unloaded || ($chunk = $this->getChunk($chunkX, $chunkZ)) === null || $chunk->isLightPopulated() === true){
1363 return;
1364 }
1365 //TODO: calculated light information might not be valid if the terrain changed during light calculation
1366
1367 $chunk->setHeightMapArray($heightMap);
1368 foreach($blockLight as $y => $lightArray){
1369 $chunk->getSubChunk($y)->setBlockLightArray($lightArray);
1370 }
1371 foreach($skyLight as $y => $lightArray){
1372 $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray);
1373 }
1374 $chunk->setLightPopulated(true);
1375 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1376 }
1377 ));
1378 }
1379 }
1380
1381 private function tickChunk(int $chunkX, int $chunkZ) : void{
1382 $chunk = $this->getChunk($chunkX, $chunkZ);
1383 if($chunk === null){
1384 //the chunk may have been unloaded during a previous chunk's update (e.g. during BlockGrowEvent)
1385 return;
1386 }
1387 foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){
1388 $entity->onRandomUpdate();
1389 }
1390
1391 $blockFactory = $this->blockStateRegistry;
1392 foreach($chunk->getSubChunks() as $Y => $subChunk){
1393 if(!$subChunk->isEmptyFast()){
1394 $k = 0;
1395 for($i = 0; $i < $this->tickedBlocksPerSubchunkPerTick; ++$i){
1396 if(($i % 5) === 0){
1397 //60 bits will be used by 5 blocks (12 bits each)
1398 $k = mt_rand(0, (1 << 60) - 1);
1399 }
1400 $x = $k & SubChunk::COORD_MASK;
1401 $y = ($k >> SubChunk::COORD_BIT_SIZE) & SubChunk::COORD_MASK;
1402 $z = ($k >> (SubChunk::COORD_BIT_SIZE * 2)) & SubChunk::COORD_MASK;
1403 $k >>= (SubChunk::COORD_BIT_SIZE * 3);
1404
1405 $state = $subChunk->getBlockStateId($x, $y, $z);
1406
1407 if(isset($this->randomTickBlocks[$state])){
1408 $block = $blockFactory->fromStateId($state);
1409 $block->position($this, $chunkX * Chunk::EDGE_LENGTH + $x, ($Y << SubChunk::COORD_BIT_SIZE) + $y, $chunkZ * Chunk::EDGE_LENGTH + $z);
1410 $block->onRandomTick();
1411 }
1412 }
1413 }
1414 }
1415 }
1416
1420 public function __debugInfo() : array{
1421 return [];
1422 }
1423
1424 public function save(bool $force = false) : bool{
1425
1426 if(!$this->getAutoSave() && !$force){
1427 return false;
1428 }
1429
1430 (new WorldSaveEvent($this))->call();
1431
1432 $timings = $this->timings->syncDataSave;
1433 $timings->startTiming();
1434
1435 $this->provider->getWorldData()->setTime($this->time);
1436 $this->saveChunks();
1437 $this->provider->getWorldData()->save();
1438
1439 $timings->stopTiming();
1440
1441 return true;
1442 }
1443
1444 public function saveChunks() : void{
1445 $this->timings->syncChunkSave->startTiming();
1446 try{
1447 foreach($this->chunks as $chunkHash => $chunk){
1448 self::getXZ($chunkHash, $chunkX, $chunkZ);
1449 $this->provider->saveChunk($chunkX, $chunkZ, new ChunkData(
1450 $chunk->getSubChunks(),
1451 $chunk->isPopulated(),
1452 array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk()))),
1453 array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())),
1454 ), $chunk->getTerrainDirtyFlags());
1455 $chunk->clearTerrainDirtyFlags();
1456 }
1457 }finally{
1458 $this->timings->syncChunkSave->stopTiming();
1459 }
1460 }
1461
1466 public function scheduleDelayedBlockUpdate(Vector3 $pos, int $delay) : void{
1467 if(
1468 !$this->isInWorld($pos->x, $pos->y, $pos->z) ||
1469 (isset($this->scheduledBlockUpdateQueueIndex[$index = World::blockHash($pos->x, $pos->y, $pos->z)]) && $this->scheduledBlockUpdateQueueIndex[$index] <= $delay)
1470 ){
1471 return;
1472 }
1473 $this->scheduledBlockUpdateQueueIndex[$index] = $delay;
1474 $this->scheduledBlockUpdateQueue->insert(new Vector3((int) $pos->x, (int) $pos->y, (int) $pos->z), $delay + $this->server->getTick());
1475 }
1476
1477 private function tryAddToNeighbourUpdateQueue(int $x, int $y, int $z) : void{
1478 if($this->isInWorld($x, $y, $z)){
1479 $hash = World::blockHash($x, $y, $z);
1480 if(!isset($this->neighbourBlockUpdateQueueIndex[$hash])){
1481 $this->neighbourBlockUpdateQueue->enqueue($hash);
1482 $this->neighbourBlockUpdateQueueIndex[$hash] = true;
1483 }
1484 }
1485 }
1486
1493 public function notifyNeighbourBlockUpdate(int $x, int $y, int $z) : void{
1494 $this->tryAddToNeighbourUpdateQueue($x, $y, $z);
1495 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1496 $this->tryAddToNeighbourUpdateQueue($x + $dx, $y + $dy, $z + $dz);
1497 }
1498 }
1499
1504 public function getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst = false) : array{
1505 $minX = (int) floor($bb->minX - 1);
1506 $minY = (int) floor($bb->minY - 1);
1507 $minZ = (int) floor($bb->minZ - 1);
1508 $maxX = (int) floor($bb->maxX + 1);
1509 $maxY = (int) floor($bb->maxY + 1);
1510 $maxZ = (int) floor($bb->maxZ + 1);
1511
1512 $collides = [];
1513
1514 $collisionInfo = $this->blockStateRegistry->collisionInfo;
1515 if($targetFirst){
1516 for($z = $minZ; $z <= $maxZ; ++$z){
1517 $zOverflow = $z === $minZ || $z === $maxZ;
1518 for($x = $minX; $x <= $maxX; ++$x){
1519 $zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
1520 for($y = $minY; $y <= $maxY; ++$y){
1521 $overflow = $zxOverflow || $y === $minY || $y === $maxY;
1522
1523 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1524 if($overflow ?
1525 $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
1526 match ($stateCollisionInfo) {
1527 RuntimeBlockStateRegistry::COLLISION_CUBE => true,
1528 RuntimeBlockStateRegistry::COLLISION_NONE => false,
1529 default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
1530 }
1531 ){
1532 return [$this->getBlockAt($x, $y, $z)];
1533 }
1534 }
1535 }
1536 }
1537 }else{
1538 //TODO: duplicated code :( this way is better for performance though
1539 for($z = $minZ; $z <= $maxZ; ++$z){
1540 $zOverflow = $z === $minZ || $z === $maxZ;
1541 for($x = $minX; $x <= $maxX; ++$x){
1542 $zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
1543 for($y = $minY; $y <= $maxY; ++$y){
1544 $overflow = $zxOverflow || $y === $minY || $y === $maxY;
1545
1546 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1547 if($overflow ?
1548 $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
1549 match ($stateCollisionInfo) {
1550 RuntimeBlockStateRegistry::COLLISION_CUBE => true,
1551 RuntimeBlockStateRegistry::COLLISION_NONE => false,
1552 default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
1553 }
1554 ){
1555 $collides[] = $this->getBlockAt($x, $y, $z);
1556 }
1557 }
1558 }
1559 }
1560 }
1561
1562 return $collides;
1563 }
1564
1569 private function getBlockCollisionInfo(int $x, int $y, int $z, array $collisionInfo) : int{
1570 if(!$this->isInWorld($x, $y, $z)){
1571 return RuntimeBlockStateRegistry::COLLISION_NONE;
1572 }
1573 $chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1574 if($chunk === null){
1575 return RuntimeBlockStateRegistry::COLLISION_NONE;
1576 }
1577 $stateId = $chunk
1578 ->getSubChunk($y >> SubChunk::COORD_BIT_SIZE)
1579 ->getBlockStateId(
1580 $x & SubChunk::COORD_MASK,
1581 $y & SubChunk::COORD_MASK,
1582 $z & SubChunk::COORD_MASK
1583 );
1584 return $collisionInfo[$stateId];
1585 }
1586
1598 private function getBlockCollisionBoxesForCell(int $x, int $y, int $z, array $collisionInfo) : array{
1599 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1600 $boxes = match($stateCollisionInfo){
1601 RuntimeBlockStateRegistry::COLLISION_NONE => [],
1602 RuntimeBlockStateRegistry::COLLISION_CUBE => [AxisAlignedBB::one()->offsetCopy($x, $y, $z)],
1603 default => $this->getBlockAt($x, $y, $z)->getCollisionBoxes()
1604 };
1605
1606 //overlapping AABBs can't make any difference if this is a cube, so we can save some CPU cycles in this common case
1607 if($stateCollisionInfo !== RuntimeBlockStateRegistry::COLLISION_CUBE){
1608 $cellBB = null;
1609 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1610 $offsetX = $x + $dx;
1611 $offsetY = $y + $dy;
1612 $offsetZ = $z + $dz;
1613 $stateCollisionInfo = $this->getBlockCollisionInfo($offsetX, $offsetY, $offsetZ, $collisionInfo);
1614 if($stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW){
1615 //avoid allocating this unless it's needed
1616 $cellBB ??= AxisAlignedBB::one()->offsetCopy($x, $y, $z);
1617 $extraBoxes = $this->getBlockAt($offsetX, $offsetY, $offsetZ)->getCollisionBoxes();
1618 foreach($extraBoxes as $extraBox){
1619 if($extraBox->intersectsWith($cellBB)){
1620 $boxes[] = $extraBox;
1621 }
1622 }
1623 }
1624 }
1625 }
1626
1627 return $boxes;
1628 }
1629
1634 public function getBlockCollisionBoxes(AxisAlignedBB $bb) : array{
1635 $minX = (int) floor($bb->minX);
1636 $minY = (int) floor($bb->minY);
1637 $minZ = (int) floor($bb->minZ);
1638 $maxX = (int) floor($bb->maxX);
1639 $maxY = (int) floor($bb->maxY);
1640 $maxZ = (int) floor($bb->maxZ);
1641
1642 $collides = [];
1643
1644 $collisionInfo = $this->blockStateRegistry->collisionInfo;
1645
1646 for($z = $minZ; $z <= $maxZ; ++$z){
1647 for($x = $minX; $x <= $maxX; ++$x){
1648 $chunkPosHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1649 for($y = $minY; $y <= $maxY; ++$y){
1650 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1651
1652 $boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z, $collisionInfo);
1653
1654 foreach($boxes as $blockBB){
1655 if($blockBB->intersectsWith($bb)){
1656 $collides[] = $blockBB;
1657 }
1658 }
1659 }
1660 }
1661 }
1662
1663 return $collides;
1664 }
1665
1670 public function computeSunAnglePercentage() : float{
1671 $timeProgress = ($this->time % self::TIME_FULL) / self::TIME_FULL;
1672
1673 //0.0 needs to be high noon, not dusk
1674 $sunProgress = $timeProgress + ($timeProgress < 0.25 ? 0.75 : -0.25);
1675
1676 //Offset the sun progress to be above the horizon longer at dusk and dawn
1677 //this is roughly an inverted sine curve, which pushes the sun progress back at dusk and forwards at dawn
1678 $diff = (((1 - ((cos($sunProgress * M_PI) + 1) / 2)) - $sunProgress) / 3);
1679
1680 return $sunProgress + $diff;
1681 }
1682
1686 public function getSunAnglePercentage() : float{
1687 return $this->sunAnglePercentage;
1688 }
1689
1693 public function getSunAngleRadians() : float{
1694 return $this->sunAnglePercentage * 2 * M_PI;
1695 }
1696
1700 public function getSunAngleDegrees() : float{
1701 return $this->sunAnglePercentage * 360.0;
1702 }
1703
1708 public function computeSkyLightReduction() : int{
1709 $percentage = max(0, min(1, -(cos($this->getSunAngleRadians()) * 2 - 0.5)));
1710
1711 //TODO: check rain and thunder level
1712
1713 return (int) ($percentage * 11);
1714 }
1715
1719 public function getSkyLightReduction() : int{
1720 return $this->skyLightReduction;
1721 }
1722
1727 public function getFullLight(Vector3 $pos) : int{
1728 $floorX = $pos->getFloorX();
1729 $floorY = $pos->getFloorY();
1730 $floorZ = $pos->getFloorZ();
1731 return $this->getFullLightAt($floorX, $floorY, $floorZ);
1732 }
1733
1738 public function getFullLightAt(int $x, int $y, int $z) : int{
1739 $skyLight = $this->getRealBlockSkyLightAt($x, $y, $z);
1740 if($skyLight < 15){
1741 return max($skyLight, $this->getBlockLightAt($x, $y, $z));
1742 }else{
1743 return $skyLight;
1744 }
1745 }
1746
1751 public function getHighestAdjacentFullLightAt(int $x, int $y, int $z) : int{
1752 return $this->getHighestAdjacentLight($x, $y, $z, $this->getFullLightAt(...));
1753 }
1754
1759 public function getPotentialLight(Vector3 $pos) : int{
1760 $floorX = $pos->getFloorX();
1761 $floorY = $pos->getFloorY();
1762 $floorZ = $pos->getFloorZ();
1763 return $this->getPotentialLightAt($floorX, $floorY, $floorZ);
1764 }
1765
1770 public function getPotentialLightAt(int $x, int $y, int $z) : int{
1771 return max($this->getPotentialBlockSkyLightAt($x, $y, $z), $this->getBlockLightAt($x, $y, $z));
1772 }
1773
1778 public function getHighestAdjacentPotentialLightAt(int $x, int $y, int $z) : int{
1779 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialLightAt(...));
1780 }
1781
1788 public function getPotentialBlockSkyLightAt(int $x, int $y, int $z) : int{
1789 if(!$this->isInWorld($x, $y, $z)){
1790 return $y >= self::Y_MAX ? 15 : 0;
1791 }
1792 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){
1793 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockSkyLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1794 }
1795 return 0; //TODO: this should probably throw instead (light not calculated yet)
1796 }
1797
1803 public function getRealBlockSkyLightAt(int $x, int $y, int $z) : int{
1804 $light = $this->getPotentialBlockSkyLightAt($x, $y, $z) - $this->skyLightReduction;
1805 return $light < 0 ? 0 : $light;
1806 }
1807
1813 public function getBlockLightAt(int $x, int $y, int $z) : int{
1814 if(!$this->isInWorld($x, $y, $z)){
1815 return 0;
1816 }
1817 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){
1818 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1819 }
1820 return 0; //TODO: this should probably throw instead (light not calculated yet)
1821 }
1822
1823 public function updateAllLight(int $x, int $y, int $z) : void{
1824 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) === null || $chunk->isLightPopulated() !== true){
1825 return;
1826 }
1827
1828 $blockFactory = $this->blockStateRegistry;
1829 $this->timings->doBlockSkyLightUpdates->startTiming();
1830 if($this->skyLightUpdate === null){
1831 $this->skyLightUpdate = new SkyLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight);
1832 }
1833 $this->skyLightUpdate->recalculateNode($x, $y, $z);
1834 $this->timings->doBlockSkyLightUpdates->stopTiming();
1835
1836 $this->timings->doBlockLightUpdates->startTiming();
1837 if($this->blockLightUpdate === null){
1838 $this->blockLightUpdate = new BlockLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->light);
1839 }
1840 $this->blockLightUpdate->recalculateNode($x, $y, $z);
1841 $this->timings->doBlockLightUpdates->stopTiming();
1842 }
1843
1847 private function getHighestAdjacentLight(int $x, int $y, int $z, \Closure $lightGetter) : int{
1848 $max = 0;
1849 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
1850 $x1 = $x + $offsetX;
1851 $y1 = $y + $offsetY;
1852 $z1 = $z + $offsetZ;
1853 if(
1854 !$this->isInWorld($x1, $y1, $z1) ||
1855 ($chunk = $this->getChunk($x1 >> Chunk::COORD_BIT_SIZE, $z1 >> Chunk::COORD_BIT_SIZE)) === null ||
1856 $chunk->isLightPopulated() !== true
1857 ){
1858 continue;
1859 }
1860 $max = max($max, $lightGetter($x1, $y1, $z1));
1861 }
1862 return $max;
1863 }
1864
1868 public function getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z) : int{
1869 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialBlockSkyLightAt(...));
1870 }
1871
1876 public function getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z) : int{
1877 return $this->getHighestAdjacentPotentialBlockSkyLight($x, $y, $z) - $this->skyLightReduction;
1878 }
1879
1883 public function getHighestAdjacentBlockLight(int $x, int $y, int $z) : int{
1884 return $this->getHighestAdjacentLight($x, $y, $z, $this->getBlockLightAt(...));
1885 }
1886
1887 private function executeQueuedLightUpdates() : void{
1888 if($this->blockLightUpdate !== null){
1889 $this->timings->doBlockLightUpdates->startTiming();
1890 $this->blockLightUpdate->execute();
1891 $this->blockLightUpdate = null;
1892 $this->timings->doBlockLightUpdates->stopTiming();
1893 }
1894
1895 if($this->skyLightUpdate !== null){
1896 $this->timings->doBlockSkyLightUpdates->startTiming();
1897 $this->skyLightUpdate->execute();
1898 $this->skyLightUpdate = null;
1899 $this->timings->doBlockSkyLightUpdates->stopTiming();
1900 }
1901 }
1902
1903 public function isInWorld(int $x, int $y, int $z) : bool{
1904 return (
1905 $x <= Limits::INT32_MAX && $x >= Limits::INT32_MIN &&
1906 $y < $this->maxY && $y >= $this->minY &&
1907 $z <= Limits::INT32_MAX && $z >= Limits::INT32_MIN
1908 );
1909 }
1910
1921 public function getBlock(Vector3 $pos, bool $cached = true, bool $addToCache = true) : Block{
1922 return $this->getBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $cached, $addToCache);
1923 }
1924
1934 public function getBlockAt(int $x, int $y, int $z, bool $cached = true, bool $addToCache = true) : Block{
1935 $relativeBlockHash = null;
1936 $chunkHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1937
1938 if($this->isInWorld($x, $y, $z)){
1939 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1940
1941 if($cached && isset($this->blockCache[$chunkHash][$relativeBlockHash])){
1942 return $this->blockCache[$chunkHash][$relativeBlockHash];
1943 }
1944
1945 $chunk = $this->chunks[$chunkHash] ?? null;
1946 if($chunk !== null){
1947 $block = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK));
1948 }else{
1949 $addToCache = false;
1950 $block = VanillaBlocks::AIR();
1951 }
1952 }else{
1953 $block = VanillaBlocks::AIR();
1954 }
1955
1956 $block->position($this, $x, $y, $z);
1957
1958 if($this->inDynamicStateRecalculation){
1959 //this call was generated by a parent getBlock() call calculating dynamic stateinfo
1960 //don't calculate dynamic state and don't add to block cache (since it won't have dynamic state calculated).
1961 //this ensures that it's impossible for dynamic state properties to recursively depend on each other.
1962 $addToCache = false;
1963 }else{
1964 $this->inDynamicStateRecalculation = true;
1965 $replacement = $block->readStateFromWorld();
1966 if($replacement !== $block){
1967 $replacement->position($this, $x, $y, $z);
1968 $block = $replacement;
1969 }
1970 $this->inDynamicStateRecalculation = false;
1971 }
1972
1973 if($addToCache && $relativeBlockHash !== null){
1974 $this->blockCache[$chunkHash][$relativeBlockHash] = $block;
1975
1976 if(++$this->blockCacheSize >= self::BLOCK_CACHE_SIZE_CAP){
1977 $this->trimBlockCache();
1978 }
1979 }
1980
1981 return $block;
1982 }
1983
1989 public function setBlock(Vector3 $pos, Block $block, bool $update = true) : void{
1990 $this->setBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $block, $update);
1991 }
1992
2001 public function setBlockAt(int $x, int $y, int $z, Block $block, bool $update = true) : void{
2002 if(!$this->isInWorld($x, $y, $z)){
2003 throw new \InvalidArgumentException("Pos x=$x,y=$y,z=$z is outside of the world bounds");
2004 }
2005 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
2006 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
2007 if($this->loadChunk($chunkX, $chunkZ) === null){ //current expected behaviour is to try to load the terrain synchronously
2008 throw new WorldException("Cannot set a block in un-generated terrain");
2009 }
2010
2011 //TODO: this computes state ID twice (we do it again in writeStateToWorld()). Not great for performance :(
2012 $stateId = $block->getStateId();
2013 if(!$this->blockStateRegistry->hasStateId($stateId)){
2014 throw new \LogicException("Block state ID not known to RuntimeBlockStateRegistry (probably not registered)");
2015 }
2016 if(!GlobalBlockStateHandlers::getSerializer()->isRegistered($block)){
2017 throw new \LogicException("Block not registered with GlobalBlockStateHandlers serializer");
2018 }
2019
2020 $this->timings->setBlock->startTiming();
2021
2022 $this->unlockChunk($chunkX, $chunkZ, null);
2023
2024 $block = clone $block;
2025
2026 $block->position($this, $x, $y, $z);
2027 $block->writeStateToWorld();
2028 $pos = new Vector3($x, $y, $z);
2029
2030 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2031 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
2032
2033 unset($this->blockCache[$chunkHash][$relativeBlockHash]);
2034 $this->blockCacheSize--;
2035 unset($this->blockCollisionBoxCache[$chunkHash][$relativeBlockHash]);
2036 //blocks like fences have collision boxes that reach into neighbouring blocks, so we need to invalidate the
2037 //caches for those blocks as well
2038 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
2039 $sideChunkPosHash = World::chunkHash(($x + $offsetX) >> Chunk::COORD_BIT_SIZE, ($z + $offsetZ) >> Chunk::COORD_BIT_SIZE);
2040 $sideChunkBlockHash = World::chunkBlockHash($x + $offsetX, $y + $offsetY, $z + $offsetZ);
2041 unset($this->blockCollisionBoxCache[$sideChunkPosHash][$sideChunkBlockHash]);
2042 }
2043
2044 if(!isset($this->changedBlocks[$chunkHash])){
2045 $this->changedBlocks[$chunkHash] = [];
2046 }
2047 $this->changedBlocks[$chunkHash][$relativeBlockHash] = $pos;
2048
2049 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2050 $listener->onBlockChanged($pos);
2051 }
2052
2053 if($update){
2054 $this->updateAllLight($x, $y, $z);
2055 $this->notifyNeighbourBlockUpdate($x, $y, $z);
2056 }
2057
2058 $this->timings->setBlock->stopTiming();
2059 }
2060
2061 public function dropItem(Vector3 $source, Item $item, ?Vector3 $motion = null, int $delay = 10) : ?ItemEntity{
2062 if($item->isNull()){
2063 return null;
2064 }
2065
2066 $itemEntity = new ItemEntity(Location::fromObject($source, $this, Utils::getRandomFloat() * 360, 0), $item);
2067
2068 $itemEntity->setPickupDelay($delay);
2069 $itemEntity->setMotion($motion ?? new Vector3(Utils::getRandomFloat() * 0.2 - 0.1, 0.2, Utils::getRandomFloat() * 0.2 - 0.1));
2070 $itemEntity->spawnToAll();
2071
2072 return $itemEntity;
2073 }
2074
2081 public function dropExperience(Vector3 $pos, int $amount) : array{
2082 $orbs = [];
2083
2084 foreach(ExperienceOrb::splitIntoOrbSizes($amount) as $split){
2085 $orb = new ExperienceOrb(Location::fromObject($pos, $this, Utils::getRandomFloat() * 360, 0), $split);
2086
2087 $orb->setMotion(new Vector3((Utils::getRandomFloat() * 0.2 - 0.1) * 2, Utils::getRandomFloat() * 0.4, (Utils::getRandomFloat() * 0.2 - 0.1) * 2));
2088 $orb->spawnToAll();
2089
2090 $orbs[] = $orb;
2091 }
2092
2093 return $orbs;
2094 }
2095
2104 public function useBreakOn(Vector3 $vector, ?Item &$item = null, ?Player $player = null, bool $createParticles = false, array &$returnedItems = []) : bool{
2105 $vector = $vector->floor();
2106
2107 $chunkX = $vector->getFloorX() >> Chunk::COORD_BIT_SIZE;
2108 $chunkZ = $vector->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2109 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2110 return false;
2111 }
2112
2113 $target = $this->getBlock($vector);
2114 $affectedBlocks = $target->getAffectedBlocks();
2115
2116 if($item === null){
2117 $item = VanillaItems::AIR();
2118 }
2119
2120 $drops = [];
2121 if($player === null || $player->hasFiniteResources()){
2122 $drops = array_merge(...array_map(fn(Block $block) => $block->getDrops($item), $affectedBlocks));
2123 }
2124
2125 $xpDrop = 0;
2126 if($player !== null && $player->hasFiniteResources()){
2127 $xpDrop = array_sum(array_map(fn(Block $block) => $block->getXpDropForTool($item), $affectedBlocks));
2128 }
2129
2130 if($player !== null){
2131 $ev = new BlockBreakEvent($player, $target, $item, $player->isCreative(), $drops, $xpDrop);
2132
2133 if($target instanceof Air || ($player->isSurvival() && !$target->getBreakInfo()->isBreakable()) || $player->isSpectator()){
2134 $ev->cancel();
2135 }
2136
2137 if($player->isAdventure(true) && !$ev->isCancelled()){
2138 $canBreak = false;
2139 $itemParser = LegacyStringToItemParser::getInstance();
2140 foreach($item->getCanDestroy() as $v){
2141 $entry = $itemParser->parse($v);
2142 if($entry->getBlock()->hasSameTypeId($target)){
2143 $canBreak = true;
2144 break;
2145 }
2146 }
2147
2148 if(!$canBreak){
2149 $ev->cancel();
2150 }
2151 }
2152
2153 $ev->call();
2154 if($ev->isCancelled()){
2155 return false;
2156 }
2157
2158 $drops = $ev->getDrops();
2159 $xpDrop = $ev->getXpDropAmount();
2160
2161 }elseif(!$target->getBreakInfo()->isBreakable()){
2162 return false;
2163 }
2164
2165 foreach($affectedBlocks as $t){
2166 $this->destroyBlockInternal($t, $item, $player, $createParticles, $returnedItems);
2167 }
2168
2169 $item->onDestroyBlock($target, $returnedItems);
2170
2171 if(count($drops) > 0){
2172 $dropPos = $vector->add(0.5, 0.5, 0.5);
2173 foreach($drops as $drop){
2174 if(!$drop->isNull()){
2175 $this->dropItem($dropPos, $drop);
2176 }
2177 }
2178 }
2179
2180 if($xpDrop > 0){
2181 $this->dropExperience($vector->add(0.5, 0.5, 0.5), $xpDrop);
2182 }
2183
2184 return true;
2185 }
2186
2190 private function destroyBlockInternal(Block $target, Item $item, ?Player $player, bool $createParticles, array &$returnedItems) : void{
2191 if($createParticles){
2192 $this->addParticle($target->getPosition()->add(0.5, 0.5, 0.5), new BlockBreakParticle($target));
2193 }
2194
2195 $target->onBreak($item, $player, $returnedItems);
2196
2197 $tile = $this->getTile($target->getPosition());
2198 if($tile !== null){
2199 $tile->onBlockDestroyed();
2200 }
2201 }
2202
2210 public function useItemOn(Vector3 $vector, Item &$item, Facing $face, ?Vector3 $clickVector = null, ?Player $player = null, bool $playSound = false, array &$returnedItems = []) : bool{
2211 $blockClicked = $this->getBlock($vector);
2212 $blockReplace = $blockClicked->getSide($face);
2213
2214 if($clickVector === null){
2215 $clickVector = new Vector3(0.0, 0.0, 0.0);
2216 }else{
2217 $clickVector = new Vector3(
2218 min(1.0, max(0.0, $clickVector->x)),
2219 min(1.0, max(0.0, $clickVector->y)),
2220 min(1.0, max(0.0, $clickVector->z))
2221 );
2222 }
2223
2224 if(!$this->isInWorld($blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z)){
2225 //TODO: build height limit messages for custom world heights and mcregion cap
2226 return false;
2227 }
2228 $chunkX = $blockReplace->getPosition()->getFloorX() >> Chunk::COORD_BIT_SIZE;
2229 $chunkZ = $blockReplace->getPosition()->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2230 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2231 return false;
2232 }
2233
2234 if($blockClicked->getTypeId() === BlockTypeIds::AIR){
2235 return false;
2236 }
2237
2238 if($player !== null){
2239 $ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK);
2240 if($player->isSneaking()){
2241 $ev->setUseItem(false);
2242 $ev->setUseBlock($item->isNull()); //opening doors is still possible when sneaking if using an empty hand
2243 }
2244 if($player->isSpectator()){
2245 $ev->cancel(); //set it to cancelled so plugins can bypass this
2246 }
2247
2248 $ev->call();
2249 if(!$ev->isCancelled()){
2250 if($ev->useBlock() && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2251 return true;
2252 }
2253
2254 if($ev->useItem()){
2255 $result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems);
2256 if($result !== ItemUseResult::NONE){
2257 return $result === ItemUseResult::SUCCESS;
2258 }
2259 }
2260 }else{
2261 return false;
2262 }
2263 }elseif($blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2264 return true;
2265 }
2266
2267 if($item->isNull() || !$item->canBePlaced()){
2268 return false;
2269 }
2270
2271 //TODO: while passing Facing::UP mimics the vanilla behaviour with replaceable blocks, we should really pass
2272 //some other value like NULL and let place() deal with it. This will look like a bug to anyone who doesn't know
2273 //about the vanilla behaviour.
2274 $tx =
2275 $item->getPlacementTransaction($blockClicked, $blockClicked, Facing::UP, $clickVector, $player) ??
2276 $item->getPlacementTransaction($blockReplace, $blockClicked, $face, $clickVector, $player);
2277 if($tx === null){
2278 //no placement options available
2279 return false;
2280 }
2281
2282 foreach($tx->getBlocks() as [$x, $y, $z, $block]){
2283 $block->position($this, $x, $y, $z);
2284 foreach($block->getCollisionBoxes() as $collisionBox){
2285 if(count($this->getCollidingEntities($collisionBox)) > 0){
2286 return false; //Entity in block
2287 }
2288 }
2289 }
2290
2291 if($player !== null){
2292 $ev = new BlockPlaceEvent($player, $tx, $blockClicked, $item);
2293 if($player->isSpectator()){
2294 $ev->cancel();
2295 }
2296
2297 if($player->isAdventure(true) && !$ev->isCancelled()){
2298 $canPlace = false;
2299 $itemParser = LegacyStringToItemParser::getInstance();
2300 foreach($item->getCanPlaceOn() as $v){
2301 $entry = $itemParser->parse($v);
2302 if($entry->getBlock()->hasSameTypeId($blockClicked)){
2303 $canPlace = true;
2304 break;
2305 }
2306 }
2307
2308 if(!$canPlace){
2309 $ev->cancel();
2310 }
2311 }
2312
2313 $ev->call();
2314 if($ev->isCancelled()){
2315 return false;
2316 }
2317 }
2318
2319 if(!$tx->apply()){
2320 return false;
2321 }
2322 $first = true;
2323 foreach($tx->getBlocks() as [$x, $y, $z, $_]){
2324 $tile = $this->getTileAt($x, $y, $z);
2325 if($tile !== null){
2326 //TODO: seal this up inside block placement
2327 $tile->copyDataFromItem($item);
2328 }
2329
2330 $placed = $this->getBlockAt($x, $y, $z);
2331 $placed->onPostPlace();
2332 if($first && $playSound){
2333 $this->addSound($placed->getPosition(), new BlockPlaceSound($placed));
2334 }
2335 $first = false;
2336 }
2337
2338 $item->pop();
2339
2340 return true;
2341 }
2342
2343 public function getEntity(int $entityId) : ?Entity{
2344 return $this->entities[$entityId] ?? null;
2345 }
2346
2353 public function getEntities() : array{
2354 return $this->entities;
2355 }
2356
2367 public function getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
2368 $nearby = [];
2369
2370 foreach($this->getNearbyEntities($bb, $entity) as $ent){
2371 if($ent->canBeCollidedWith() && ($entity === null || $entity->canCollideWith($ent))){
2372 $nearby[] = $ent;
2373 }
2374 }
2375
2376 return $nearby;
2377 }
2378
2385 public function getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
2386 $nearby = [];
2387
2388 $minX = ((int) floor($bb->minX - 2)) >> Chunk::COORD_BIT_SIZE;
2389 $maxX = ((int) floor($bb->maxX + 2)) >> Chunk::COORD_BIT_SIZE;
2390 $minZ = ((int) floor($bb->minZ - 2)) >> Chunk::COORD_BIT_SIZE;
2391 $maxZ = ((int) floor($bb->maxZ + 2)) >> Chunk::COORD_BIT_SIZE;
2392
2393 for($x = $minX; $x <= $maxX; ++$x){
2394 for($z = $minZ; $z <= $maxZ; ++$z){
2395 foreach($this->getChunkEntities($x, $z) as $ent){
2396 if($ent !== $entity && $ent->boundingBox->intersectsWith($bb)){
2397 $nearby[] = $ent;
2398 }
2399 }
2400 }
2401 }
2402
2403 return $nearby;
2404 }
2405
2417 public function getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType = Entity::class, bool $includeDead = false) : ?Entity{
2418 assert(is_a($entityType, Entity::class, true));
2419
2420 $minX = ((int) floor($pos->x - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2421 $maxX = ((int) floor($pos->x + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2422 $minZ = ((int) floor($pos->z - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2423 $maxZ = ((int) floor($pos->z + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2424
2425 $currentTargetDistSq = $maxDistance ** 2;
2426
2431 $currentTarget = null;
2432
2433 for($x = $minX; $x <= $maxX; ++$x){
2434 for($z = $minZ; $z <= $maxZ; ++$z){
2435 foreach($this->getChunkEntities($x, $z) as $entity){
2436 if(!($entity instanceof $entityType) || $entity->isFlaggedForDespawn() || (!$includeDead && !$entity->isAlive())){
2437 continue;
2438 }
2439 $distSq = $entity->getPosition()->distanceSquared($pos);
2440 if($distSq < $currentTargetDistSq){
2441 $currentTargetDistSq = $distSq;
2442 $currentTarget = $entity;
2443 }
2444 }
2445 }
2446 }
2447
2448 return $currentTarget;
2449 }
2450
2457 public function getPlayers() : array{
2458 return $this->players;
2459 }
2460
2467 public function getTile(Vector3 $pos) : ?Tile{
2468 return $this->getTileAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z));
2469 }
2470
2474 public function getTileAt(int $x, int $y, int $z) : ?Tile{
2475 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;
2476 }
2477
2478 public function getBiomeId(int $x, int $y, int $z) : int{
2479 if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
2480 return $chunk->getBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2481 }
2482 return BiomeIds::OCEAN; //TODO: this should probably throw instead (terrain not generated yet)
2483 }
2484
2485 public function getBiome(int $x, int $y, int $z) : Biome{
2486 return BiomeRegistry::getInstance()->getBiome($this->getBiomeId($x, $y, $z));
2487 }
2488
2489 public function setBiomeId(int $x, int $y, int $z, int $biomeId) : void{
2490 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
2491 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
2492 $this->unlockChunk($chunkX, $chunkZ, null);
2493 if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
2494 $chunk->setBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId);
2495 }else{
2496 //if we allowed this, the modifications would be lost when the chunk is created
2497 throw new WorldException("Cannot set biome in a non-generated chunk");
2498 }
2499 }
2500
2505 public function getLoadedChunks() : array{
2506 return $this->chunks;
2507 }
2508
2509 public function getChunk(int $chunkX, int $chunkZ) : ?Chunk{
2510 return $this->chunks[World::chunkHash($chunkX, $chunkZ)] ?? null;
2511 }
2512
2517 public function getChunkEntities(int $chunkX, int $chunkZ) : array{
2518 return $this->entitiesByChunk[World::chunkHash($chunkX, $chunkZ)] ?? [];
2519 }
2520
2524 public function getOrLoadChunkAtPosition(Vector3 $pos) : ?Chunk{
2525 return $this->loadChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2526 }
2527
2534 public function getAdjacentChunks(int $x, int $z) : array{
2535 $result = [];
2536 for($xx = -1; $xx <= 1; ++$xx){
2537 for($zz = -1; $zz <= 1; ++$zz){
2538 if($xx === 0 && $zz === 0){
2539 continue; //center chunk
2540 }
2541 $result[World::chunkHash($xx, $zz)] = $this->loadChunk($x + $xx, $z + $zz);
2542 }
2543 }
2544
2545 return $result;
2546 }
2547
2562 public function lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId) : void{
2563 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2564 if(isset($this->chunkLock[$chunkHash])){
2565 throw new \InvalidArgumentException("Chunk $chunkX $chunkZ is already locked");
2566 }
2567 $this->chunkLock[$chunkHash] = $lockId;
2568 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2569 }
2570
2579 public function unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId) : bool{
2580 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2581 if(isset($this->chunkLock[$chunkHash]) && ($lockId === null || $this->chunkLock[$chunkHash] === $lockId)){
2582 unset($this->chunkLock[$chunkHash]);
2583 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2584 return true;
2585 }
2586 return false;
2587 }
2588
2594 public function isChunkLocked(int $chunkX, int $chunkZ) : bool{
2595 return isset($this->chunkLock[World::chunkHash($chunkX, $chunkZ)]);
2596 }
2597
2598 public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk) : void{
2599 foreach($chunk->getSubChunks() as $subChunk){
2600 foreach($subChunk->getBlockLayers() as $blockLayer){
2601 foreach($blockLayer->getPalette() as $blockStateId){
2602 if(!$this->blockStateRegistry->hasStateId($blockStateId)){
2603 throw new \InvalidArgumentException("Provided chunk contains unknown/unregistered blocks (found unknown state ID $blockStateId)");
2604 }
2605 }
2606 }
2607 }
2608
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 unset($this->knownUngeneratedChunks[$chunkHash]);
2642
2643 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
2644 unset($this->blockCache[$chunkHash]);
2645 unset($this->blockCollisionBoxCache[$chunkHash]);
2646 unset($this->changedBlocks[$chunkHash]);
2647 $chunk->setTerrainDirty();
2648 $this->markTickingChunkForRecheck($chunkX, $chunkZ); //this replacement chunk may not meet the conditions for ticking
2649
2650 if(!$this->isChunkInUse($chunkX, $chunkZ)){
2651 $this->unloadChunkRequest($chunkX, $chunkZ);
2652 }
2653
2654 if($oldChunk === null){
2655 if(ChunkLoadEvent::hasHandlers()){
2656 (new ChunkLoadEvent($this, $chunkX, $chunkZ, $chunk, true))->call();
2657 }
2658
2659 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2660 $listener->onChunkLoaded($chunkX, $chunkZ, $chunk);
2661 }
2662 }else{
2663 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2664 $listener->onChunkChanged($chunkX, $chunkZ, $chunk);
2665 }
2666 }
2667
2668 for($cX = -1; $cX <= 1; ++$cX){
2669 for($cZ = -1; $cZ <= 1; ++$cZ){
2670 foreach($this->getChunkEntities($chunkX + $cX, $chunkZ + $cZ) as $entity){
2671 $entity->onNearbyBlockChange();
2672 }
2673 }
2674 }
2675 }
2676
2683 public function getHighestBlockAt(int $x, int $z) : ?int{
2684 if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
2685 return $chunk->getHighestBlockAt($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2686 }
2687 throw new WorldException("Cannot get highest block in an ungenerated chunk");
2688 }
2689
2693 public function isInLoadedTerrain(Vector3 $pos) : bool{
2694 return $this->isChunkLoaded($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2695 }
2696
2697 public function isChunkLoaded(int $x, int $z) : bool{
2698 return isset($this->chunks[World::chunkHash($x, $z)]);
2699 }
2700
2701 public function isChunkGenerated(int $x, int $z) : bool{
2702 return $this->loadChunk($x, $z) !== null;
2703 }
2704
2705 public function isChunkPopulated(int $x, int $z) : bool{
2706 $chunk = $this->loadChunk($x, $z);
2707 return $chunk !== null && $chunk->isPopulated();
2708 }
2709
2713 public function getSpawnLocation() : Position{
2714 return Position::fromObject($this->provider->getWorldData()->getSpawn(), $this);
2715 }
2716
2720 public function setSpawnLocation(Vector3 $pos) : void{
2721 $previousSpawn = $this->getSpawnLocation();
2722 $this->provider->getWorldData()->setSpawn($pos);
2723 (new SpawnChangeEvent($this, $previousSpawn))->call();
2724
2725 $location = Position::fromObject($pos, $this);
2726 foreach($this->players as $player){
2727 $player->getNetworkSession()->syncWorldSpawnPoint($location);
2728 }
2729 }
2730
2734 public function addEntity(Entity $entity) : void{
2735 if($entity->isClosed()){
2736 throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to world");
2737 }
2738 if($entity->getWorld() !== $this){
2739 throw new \InvalidArgumentException("Invalid Entity world");
2740 }
2741 if(array_key_exists($entity->getId(), $this->entities)){
2742 if($this->entities[$entity->getId()] === $entity){
2743 throw new \InvalidArgumentException("Entity " . $entity->getId() . " has already been added to this world");
2744 }else{
2745 throw new AssumptionFailedError("Found two different entities sharing entity ID " . $entity->getId());
2746 }
2747 }
2748 if(!EntityFactory::getInstance()->isRegistered($entity::class) && !$entity instanceof NeverSavedWithChunkEntity){
2749 //canSaveWithChunk is mutable, so that means it could be toggled after adding the entity and cause a crash
2750 //later on. Better we just force all entities to have a save ID, even if it might not be needed.
2751 throw new \LogicException("Entity " . $entity::class . " is not registered for a save ID in EntityFactory");
2752 }
2753 $pos = $entity->getPosition()->asVector3();
2754 $this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity;
2755 $this->entityLastKnownPositions[$entity->getId()] = $pos;
2756
2757 if($entity instanceof Player){
2758 $this->players[$entity->getId()] = $entity;
2759 }
2760 $this->entities[$entity->getId()] = $entity;
2761 }
2762
2768 public function removeEntity(Entity $entity) : void{
2769 if($entity->getWorld() !== $this){
2770 throw new \InvalidArgumentException("Invalid Entity world");
2771 }
2772 if(!array_key_exists($entity->getId(), $this->entities)){
2773 throw new \InvalidArgumentException("Entity is not tracked by this world (possibly already removed?)");
2774 }
2775 $pos = $this->entityLastKnownPositions[$entity->getId()];
2776 $chunkHash = World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2777 if(isset($this->entitiesByChunk[$chunkHash][$entity->getId()])){
2778 if(count($this->entitiesByChunk[$chunkHash]) === 1){
2779 unset($this->entitiesByChunk[$chunkHash]);
2780 }else{
2781 unset($this->entitiesByChunk[$chunkHash][$entity->getId()]);
2782 }
2783 }
2784 unset($this->entityLastKnownPositions[$entity->getId()]);
2785
2786 if($entity instanceof Player){
2787 unset($this->players[$entity->getId()]);
2788 $this->checkSleep();
2789 }
2790
2791 unset($this->entities[$entity->getId()]);
2792 unset($this->updateEntities[$entity->getId()]);
2793 }
2794
2798 public function onEntityMoved(Entity $entity) : void{
2799 if(!array_key_exists($entity->getId(), $this->entityLastKnownPositions)){
2800 //this can happen if the entity was teleported before addEntity() was called
2801 return;
2802 }
2803 $oldPosition = $this->entityLastKnownPositions[$entity->getId()];
2804 $newPosition = $entity->getPosition();
2805
2806 $oldChunkX = $oldPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2807 $oldChunkZ = $oldPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2808 $newChunkX = $newPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2809 $newChunkZ = $newPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2810
2811 if($oldChunkX !== $newChunkX || $oldChunkZ !== $newChunkZ){
2812 $oldChunkHash = World::chunkHash($oldChunkX, $oldChunkZ);
2813 if(isset($this->entitiesByChunk[$oldChunkHash][$entity->getId()])){
2814 if(count($this->entitiesByChunk[$oldChunkHash]) === 1){
2815 unset($this->entitiesByChunk[$oldChunkHash]);
2816 }else{
2817 unset($this->entitiesByChunk[$oldChunkHash][$entity->getId()]);
2818 }
2819 }
2820
2821 $newViewers = $this->getViewersForPosition($newPosition);
2822 foreach($entity->getViewers() as $player){
2823 if(!isset($newViewers[spl_object_id($player)])){
2824 $entity->despawnFrom($player);
2825 }else{
2826 unset($newViewers[spl_object_id($player)]);
2827 }
2828 }
2829 foreach($newViewers as $player){
2830 $entity->spawnTo($player);
2831 }
2832
2833 $newChunkHash = World::chunkHash($newChunkX, $newChunkZ);
2834 $this->entitiesByChunk[$newChunkHash][$entity->getId()] = $entity;
2835 }
2836 $this->entityLastKnownPositions[$entity->getId()] = $newPosition->asVector3();
2837 }
2838
2843 public function addTile(Tile $tile) : void{
2844 if($tile->isClosed()){
2845 throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to world");
2846 }
2847 $pos = $tile->getPosition();
2848 if(!$pos->isValid() || $pos->getWorld() !== $this){
2849 throw new \InvalidArgumentException("Invalid Tile world");
2850 }
2851 if(!$this->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ())){
2852 throw new \InvalidArgumentException("Tile position is outside the world bounds");
2853 }
2854 if(!TileFactory::getInstance()->isRegistered($tile::class)){
2855 throw new \LogicException("Tile " . $tile::class . " is not registered for a save ID in TileFactory");
2856 }
2857
2858 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2859 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2860
2861 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2862 $this->chunks[$hash]->addTile($tile);
2863 }else{
2864 throw new \InvalidArgumentException("Attempted to create tile " . get_class($tile) . " in unloaded chunk $chunkX $chunkZ");
2865 }
2866
2867 //delegate tile ticking to the corresponding block
2868 $this->scheduleDelayedBlockUpdate($pos->asVector3(), 1);
2869 }
2870
2875 public function removeTile(Tile $tile) : void{
2876 $pos = $tile->getPosition();
2877 if(!$pos->isValid() || $pos->getWorld() !== $this){
2878 throw new \InvalidArgumentException("Invalid Tile world");
2879 }
2880
2881 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2882 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2883
2884 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2885 $this->chunks[$hash]->removeTile($tile);
2886 }
2887 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2888 $listener->onBlockChanged($pos->asVector3());
2889 }
2890 }
2891
2892 public function isChunkInUse(int $x, int $z) : bool{
2893 return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) && count($this->chunkLoaders[$index]) > 0;
2894 }
2895
2902 public function loadChunk(int $x, int $z) : ?Chunk{
2903 if(isset($this->chunks[$chunkHash = World::chunkHash($x, $z)])){
2904 return $this->chunks[$chunkHash];
2905 }
2906 if(isset($this->knownUngeneratedChunks[$chunkHash])){
2907 return null;
2908 }
2909
2910 $this->timings->syncChunkLoad->startTiming();
2911
2912 $this->cancelUnloadChunkRequest($x, $z);
2913
2914 $this->timings->syncChunkLoadData->startTiming();
2915
2916 $loadedChunkData = null;
2917
2918 try{
2919 $loadedChunkData = $this->provider->loadChunk($x, $z);
2920 }catch(CorruptedChunkException $e){
2921 $this->logger->critical("Failed to load chunk x=$x z=$z: " . $e->getMessage());
2922 }
2923
2924 $this->timings->syncChunkLoadData->stopTiming();
2925
2926 if($loadedChunkData === null){
2927 $this->timings->syncChunkLoad->stopTiming();
2928 $this->knownUngeneratedChunks[$chunkHash] = true;
2929 return null;
2930 }
2931
2932 $chunkData = $loadedChunkData->getData();
2933 $chunk = new Chunk($chunkData->getSubChunks(), $chunkData->isPopulated());
2934 if(!$loadedChunkData->isUpgraded()){
2935 $chunk->clearTerrainDirtyFlags();
2936 }else{
2937 $this->logger->debug("Chunk $x $z has been upgraded, will be saved at the next autosave opportunity");
2938 }
2939 $this->chunks[$chunkHash] = $chunk;
2940
2941 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
2942 unset($this->blockCache[$chunkHash]);
2943 unset($this->blockCollisionBoxCache[$chunkHash]);
2944
2945 $this->initChunk($x, $z, $chunkData, $chunk);
2946
2947 if(ChunkLoadEvent::hasHandlers()){
2948 (new ChunkLoadEvent($this, $x, $z, $this->chunks[$chunkHash], false))->call();
2949 }
2950
2951 if(!$this->isChunkInUse($x, $z)){
2952 $this->logger->debug("Newly loaded chunk $x $z has no loaders registered, will be unloaded at next available opportunity");
2953 $this->unloadChunkRequest($x, $z);
2954 }
2955 foreach($this->getChunkListeners($x, $z) as $listener){
2956 $listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]);
2957 }
2958 $this->markTickingChunkForRecheck($x, $z); //tickers may have been registered before the chunk was loaded
2959
2960 $this->timings->syncChunkLoad->stopTiming();
2961
2962 return $this->chunks[$chunkHash];
2963 }
2964
2965 private function initChunk(int $chunkX, int $chunkZ, ChunkData $chunkData, Chunk $chunk) : void{
2966 $logger = new \PrefixedLogger($this->logger, "Loading chunk $chunkX $chunkZ");
2967
2968 if(count($chunkData->getEntityNBT()) !== 0){
2969 $this->timings->syncChunkLoadEntities->startTiming();
2970 $entityFactory = EntityFactory::getInstance();
2971
2972 $deletedEntities = [];
2973 foreach($chunkData->getEntityNBT() as $k => $nbt){
2974 try{
2975 $entity = $entityFactory->createFromData($this, $nbt);
2976 }catch(SavedDataLoadingException $e){
2977 $logger->error("Bad entity data at list position $k: " . $e->getMessage());
2978 $logger->logException($e);
2979 continue;
2980 }
2981 if($entity === null){
2982 $saveIdTag = $nbt->getTag("identifier") ?? $nbt->getTag("id");
2983 $saveId = "<unknown>";
2984 if($saveIdTag instanceof StringTag){
2985 $saveId = $saveIdTag->getValue();
2986 }elseif($saveIdTag instanceof IntTag){ //legacy MCPE format
2987 $saveId = "legacy(" . $saveIdTag->getValue() . ")";
2988 }
2989 $deletedEntities[$saveId] = ($deletedEntities[$saveId] ?? 0) + 1;
2990 }
2991 //TODO: we can't prevent entities getting added to unloaded chunks if they were saved in the wrong place
2992 //here, because entities currently add themselves to the world
2993 }
2994
2995 foreach(Utils::promoteKeys($deletedEntities) as $saveId => $count){
2996 $logger->warning("Deleted unknown entity type $saveId x$count");
2997 }
2998 $this->timings->syncChunkLoadEntities->stopTiming();
2999 }
3000
3001 if(count($chunkData->getTileNBT()) !== 0){
3002 $this->timings->syncChunkLoadTileEntities->startTiming();
3003 $tileFactory = TileFactory::getInstance();
3004
3005 $deletedTiles = [];
3006 foreach($chunkData->getTileNBT() as $k => $nbt){
3007 try{
3008 $tile = $tileFactory->createFromData($this, $nbt);
3009 }catch(SavedDataLoadingException $e){
3010 $logger->error("Bad tile entity data at list position $k: " . $e->getMessage());
3011 $logger->logException($e);
3012 continue;
3013 }
3014 if($tile === null){
3015 $saveId = $nbt->getString("id", "<unknown>");
3016 $deletedTiles[$saveId] = ($deletedTiles[$saveId] ?? 0) + 1;
3017 continue;
3018 }
3019
3020 $tilePosition = $tile->getPosition();
3021 if(!$this->isChunkLoaded($tilePosition->getFloorX() >> Chunk::COORD_BIT_SIZE, $tilePosition->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
3022 $logger->error("Found tile saved on wrong chunk - unable to fix due to correct chunk not loaded");
3023 }elseif(!$this->isInWorld($tilePosition->getFloorX(), $tilePosition->getFloorY(), $tilePosition->getFloorZ())){
3024 $logger->error("Cannot add tile with position outside the world bounds: x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z");
3025 }elseif($this->getTile($tilePosition) !== null){
3026 $logger->error("Cannot add tile at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z: Another tile is already at that position");
3027 }else{
3028 $this->addTile($tile);
3029 }
3030 $expectedStateId = $chunk->getBlockStateId($tilePosition->getFloorX() & Chunk::COORD_MASK, $tilePosition->getFloorY(), $tilePosition->getFloorZ() & Chunk::COORD_MASK);
3031 $actualStateId = $this->getBlock($tilePosition)->getStateId();
3032 if($expectedStateId !== $actualStateId){
3033 //state ID was updated by readStateFromWorld - typically because the block pulled some data from the tile
3034 //make sure this is synced to the chunk
3035 //TODO: in the future we should pull tile reading logic out of readStateFromWorld() and do it only
3036 //when the tile is loaded - this would be cleaner and faster
3037 $chunk->setBlockStateId($tilePosition->getFloorX() & Chunk::COORD_MASK, $tilePosition->getFloorY(), $tilePosition->getFloorZ() & Chunk::COORD_MASK, $actualStateId);
3038 $this->logger->debug("Tile " . $tile::class . " at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z updated block state ID from $expectedStateId to $actualStateId");
3039 }
3040 }
3041
3042 foreach(Utils::promoteKeys($deletedTiles) as $saveId => $count){
3043 $logger->warning("Deleted unknown tile entity type $saveId x$count");
3044 }
3045
3046 $this->timings->syncChunkLoadTileEntities->stopTiming();
3047 }
3048 }
3049
3050 private function queueUnloadChunk(int $x, int $z) : void{
3051 $this->unloadQueue[World::chunkHash($x, $z)] = microtime(true);
3052 }
3053
3054 public function unloadChunkRequest(int $x, int $z, bool $safe = true) : bool{
3055 if(($safe && $this->isChunkInUse($x, $z)) || $this->isSpawnChunk($x, $z)){
3056 return false;
3057 }
3058
3059 $this->queueUnloadChunk($x, $z);
3060
3061 return true;
3062 }
3063
3064 public function cancelUnloadChunkRequest(int $x, int $z) : void{
3065 unset($this->unloadQueue[World::chunkHash($x, $z)]);
3066 }
3067
3068 public function unloadChunk(int $x, int $z, bool $safe = true, bool $trySave = true) : bool{
3069 if($safe && $this->isChunkInUse($x, $z)){
3070 return false;
3071 }
3072
3073 if(!$this->isChunkLoaded($x, $z)){
3074 return true;
3075 }
3076
3077 $this->timings->doChunkUnload->startTiming();
3078
3079 $chunkHash = World::chunkHash($x, $z);
3080
3081 $chunk = $this->chunks[$chunkHash] ?? null;
3082
3083 if($chunk !== null){
3084 if(ChunkUnloadEvent::hasHandlers()){
3085 $ev = new ChunkUnloadEvent($this, $x, $z, $chunk);
3086 $ev->call();
3087 if($ev->isCancelled()){
3088 $this->timings->doChunkUnload->stopTiming();
3089
3090 return false;
3091 }
3092 }
3093
3094 if($trySave && $this->getAutoSave()){
3095 $this->timings->syncChunkSave->startTiming();
3096 try{
3097 $this->provider->saveChunk($x, $z, new ChunkData(
3098 $chunk->getSubChunks(),
3099 $chunk->isPopulated(),
3100 array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk()))),
3101 array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())),
3102 ), $chunk->getTerrainDirtyFlags());
3103 }finally{
3104 $this->timings->syncChunkSave->stopTiming();
3105 }
3106 }
3107
3108 foreach($this->getChunkListeners($x, $z) as $listener){
3109 $listener->onChunkUnloaded($x, $z, $chunk);
3110 }
3111
3112 foreach($this->getChunkEntities($x, $z) as $entity){
3113 if($entity instanceof Player){
3114 continue;
3115 }
3116 $entity->close();
3117 }
3118
3119 $chunk->onUnload();
3120 }
3121
3122 unset($this->chunks[$chunkHash]);
3123 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
3124 unset($this->blockCache[$chunkHash]);
3125 unset($this->blockCollisionBoxCache[$chunkHash]);
3126 unset($this->changedBlocks[$chunkHash]);
3127 unset($this->registeredTickingChunks[$chunkHash]);
3128 $this->markTickingChunkForRecheck($x, $z);
3129
3130 if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){
3131 $this->logger->debug("Rejecting population promise for chunk $x $z");
3132 $this->chunkPopulationRequestMap[$chunkHash]->reject();
3133 unset($this->chunkPopulationRequestMap[$chunkHash]);
3134 if(isset($this->activeChunkPopulationTasks[$chunkHash])){
3135 $this->logger->debug("Marking population task for chunk $x $z as orphaned");
3136 $this->activeChunkPopulationTasks[$chunkHash] = false;
3137 }
3138 }
3139
3140 $this->timings->doChunkUnload->stopTiming();
3141
3142 return true;
3143 }
3144
3148 public function isSpawnChunk(int $X, int $Z) : bool{
3149 $spawn = $this->getSpawnLocation();
3150 $spawnX = $spawn->x >> Chunk::COORD_BIT_SIZE;
3151 $spawnZ = $spawn->z >> Chunk::COORD_BIT_SIZE;
3152
3153 return abs($X - $spawnX) <= 1 && abs($Z - $spawnZ) <= 1;
3154 }
3155
3163 public function requestSafeSpawn(?Vector3 $spawn = null) : Promise{
3165 $resolver = new PromiseResolver();
3166 $spawn ??= $this->getSpawnLocation();
3167 /*
3168 * TODO: this relies on the assumption that getSafeSpawn() will only alter the Y coordinate of the provided
3169 * position, which is currently OK, but might be a problem in the future.
3170 */
3171 $this->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
3172 function() use ($spawn, $resolver) : void{
3173 $spawn = $this->getSafeSpawn($spawn);
3174 $resolver->resolve($spawn);
3175 },
3176 function() use ($resolver) : void{
3177 $resolver->reject();
3178 }
3179 );
3180
3181 return $resolver->getPromise();
3182 }
3183
3190 public function getSafeSpawn(?Vector3 $spawn = null) : Position{
3191 if(!($spawn instanceof Vector3) || $spawn->y <= $this->minY){
3192 $spawn = $this->getSpawnLocation();
3193 }
3194
3195 $max = $this->maxY;
3196 $v = $spawn->floor();
3197 $chunk = $this->getOrLoadChunkAtPosition($v);
3198 if($chunk === null){
3199 throw new WorldException("Cannot find a safe spawn point in non-generated terrain");
3200 }
3201 $x = (int) $v->x;
3202 $z = (int) $v->z;
3203 $y = (int) min($max - 2, $v->y);
3204 $wasAir = $this->getBlockAt($x, $y - 1, $z)->getTypeId() === BlockTypeIds::AIR; //TODO: bad hack, clean up
3205 for(; $y > $this->minY; --$y){
3206 if($this->getBlockAt($x, $y, $z)->isFullCube()){
3207 if($wasAir){
3208 $y++;
3209 }
3210 break;
3211 }else{
3212 $wasAir = true;
3213 }
3214 }
3215
3216 for(; $y >= $this->minY && $y < $max; ++$y){
3217 if(!$this->getBlockAt($x, $y + 1, $z)->isFullCube()){
3218 if(!$this->getBlockAt($x, $y, $z)->isFullCube()){
3219 return new Position($spawn->x, $y === (int) $spawn->y ? $spawn->y : $y, $spawn->z, $this);
3220 }
3221 }else{
3222 ++$y;
3223 }
3224 }
3225
3226 return new Position($spawn->x, $y, $spawn->z, $this);
3227 }
3228
3232 public function getTime() : int{
3233 return $this->time;
3234 }
3235
3239 public function getTimeOfDay() : int{
3240 return $this->time % self::TIME_FULL;
3241 }
3242
3247 public function getDisplayName() : string{
3248 return $this->displayName;
3249 }
3250
3254 public function setDisplayName(string $name) : void{
3255 (new WorldDisplayNameChangeEvent($this, $this->displayName, $name))->call();
3256
3257 $this->displayName = $name;
3258 $this->provider->getWorldData()->setName($name);
3259 }
3260
3264 public function getFolderName() : string{
3265 return $this->folderName;
3266 }
3267
3271 public function setTime(int $time) : void{
3272 $this->time = $time;
3273 $this->sendTime();
3274 }
3275
3279 public function stopTime() : void{
3280 $this->stopTime = true;
3281 $this->sendTime();
3282 }
3283
3287 public function startTime() : void{
3288 $this->stopTime = false;
3289 $this->sendTime();
3290 }
3291
3295 public function getSeed() : int{
3296 return $this->provider->getWorldData()->getSeed();
3297 }
3298
3299 public function getMinY() : int{
3300 return $this->minY;
3301 }
3302
3303 public function getMaxY() : int{
3304 return $this->maxY;
3305 }
3306
3307 public function getDifficulty() : int{
3308 return $this->provider->getWorldData()->getDifficulty();
3309 }
3310
3311 public function setDifficulty(int $difficulty) : void{
3312 if($difficulty < 0 || $difficulty > 3){
3313 throw new \InvalidArgumentException("Invalid difficulty level $difficulty");
3314 }
3315 (new WorldDifficultyChangeEvent($this, $this->getDifficulty(), $difficulty))->call();
3316 $this->provider->getWorldData()->setDifficulty($difficulty);
3317
3318 foreach($this->players as $player){
3319 $player->getNetworkSession()->syncWorldDifficulty($this->getDifficulty());
3320 }
3321 }
3322
3323 private function addChunkHashToPopulationRequestQueue(int $chunkHash) : void{
3324 if(!isset($this->chunkPopulationRequestQueueIndex[$chunkHash])){
3325 $this->chunkPopulationRequestQueue->enqueue($chunkHash);
3326 $this->chunkPopulationRequestQueueIndex[$chunkHash] = true;
3327 }
3328 }
3329
3333 private function enqueuePopulationRequest(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3334 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3335 $this->addChunkHashToPopulationRequestQueue($chunkHash);
3337 $resolver = $this->chunkPopulationRequestMap[$chunkHash] = new PromiseResolver();
3338 if($associatedChunkLoader === null){
3339 $temporaryLoader = new ChunkLoader();
3340 $this->registerChunkLoader($temporaryLoader, $chunkX, $chunkZ);
3341 $resolver->getPromise()->onCompletion(
3342 fn() => $this->unregisterChunkLoader($temporaryLoader, $chunkX, $chunkZ),
3343 static function() : void{}
3344 );
3345 }
3346 return $resolver->getPromise();
3347 }
3348
3349 private function drainPopulationRequestQueue() : void{
3350 $failed = [];
3351 while(count($this->activeChunkPopulationTasks) < $this->maxConcurrentChunkPopulationTasks && !$this->chunkPopulationRequestQueue->isEmpty()){
3352 $nextChunkHash = $this->chunkPopulationRequestQueue->dequeue();
3353 unset($this->chunkPopulationRequestQueueIndex[$nextChunkHash]);
3354 World::getXZ($nextChunkHash, $nextChunkX, $nextChunkZ);
3355 if(isset($this->chunkPopulationRequestMap[$nextChunkHash])){
3356 assert(!($this->activeChunkPopulationTasks[$nextChunkHash] ?? false), "Population for chunk $nextChunkX $nextChunkZ already running");
3357 if(
3358 !$this->orderChunkPopulation($nextChunkX, $nextChunkZ, null)->isResolved() &&
3359 !isset($this->activeChunkPopulationTasks[$nextChunkHash])
3360 ){
3361 $failed[] = $nextChunkHash;
3362 }
3363 }
3364 }
3365
3366 //these requests failed even though they weren't rate limited; we can't directly re-add them to the back of the
3367 //queue because it would result in an infinite loop
3368 foreach($failed as $hash){
3369 $this->addChunkHashToPopulationRequestQueue($hash);
3370 }
3371 }
3372
3378 private function checkChunkPopulationPreconditions(int $chunkX, int $chunkZ) : array{
3379 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3380 $resolver = $this->chunkPopulationRequestMap[$chunkHash] ?? null;
3381 if($resolver !== null && isset($this->activeChunkPopulationTasks[$chunkHash])){
3382 //generation is already running
3383 return [$resolver, false];
3384 }
3385
3386 $temporaryChunkLoader = new ChunkLoader();
3387 $this->registerChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3388 $chunk = $this->loadChunk($chunkX, $chunkZ);
3389 $this->unregisterChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3390 if($chunk !== null && $chunk->isPopulated()){
3391 //chunk is already populated; return a pre-resolved promise that will directly fire callbacks assigned
3392 $resolver ??= new PromiseResolver();
3393 unset($this->chunkPopulationRequestMap[$chunkHash]);
3394 $resolver->resolve($chunk);
3395 return [$resolver, false];
3396 }
3397 return [$resolver, true];
3398 }
3399
3411 public function requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3412 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3413 if(!$proceedWithPopulation){
3414 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3415 }
3416
3417 if(count($this->activeChunkPopulationTasks) >= $this->maxConcurrentChunkPopulationTasks){
3418 //too many chunks are already generating; delay resolution of the request until later
3419 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3420 }
3421 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3422 }
3423
3434 public function orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3435 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3436 if(!$proceedWithPopulation){
3437 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3438 }
3439
3440 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3441 }
3442
3447 private function internalOrderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader, ?PromiseResolver $resolver) : Promise{
3448 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3449
3450 $timings = $this->timings->chunkPopulationOrder;
3451 $timings->startTiming();
3452
3453 try{
3454 for($xx = -1; $xx <= 1; ++$xx){
3455 for($zz = -1; $zz <= 1; ++$zz){
3456 if($this->isChunkLocked($chunkX + $xx, $chunkZ + $zz)){
3457 //chunk is already in use by another generation request; queue the request for later
3458 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3459 }
3460 }
3461 }
3462
3463 $this->activeChunkPopulationTasks[$chunkHash] = true;
3464 if($resolver === null){
3465 $resolver = new PromiseResolver();
3466 $this->chunkPopulationRequestMap[$chunkHash] = $resolver;
3467 }
3468
3469 $chunkPopulationLockId = new ChunkLockId();
3470
3471 $temporaryChunkLoader = new ChunkLoader();
3472 for($xx = -1; $xx <= 1; ++$xx){
3473 for($zz = -1; $zz <= 1; ++$zz){
3474 $this->lockChunk($chunkX + $xx, $chunkZ + $zz, $chunkPopulationLockId);
3475 $this->registerChunkLoader($temporaryChunkLoader, $chunkX + $xx, $chunkZ + $zz);
3476 }
3477 }
3478
3479 $centerChunk = $this->loadChunk($chunkX, $chunkZ);
3480 $adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
3481
3482 $this->generatorExecutor->populate(
3483 $chunkX,
3484 $chunkZ,
3485 $centerChunk,
3486 $adjacentChunks,
3487 function(Chunk $centerChunk, array $adjacentChunks) use ($chunkPopulationLockId, $chunkX, $chunkZ, $temporaryChunkLoader) : void{
3488 if(!$this->isLoaded()){
3489 return;
3490 }
3491
3492 $this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
3493 }
3494 );
3495
3496 return $resolver->getPromise();
3497 }finally{
3498 $timings->stopTiming();
3499 }
3500 }
3501
3506 private function generateChunkCallback(ChunkLockId $chunkLockId, int $x, int $z, Chunk $chunk, array $adjacentChunks, ChunkLoader $temporaryChunkLoader) : void{
3507 $timings = $this->timings->chunkPopulationCompletion;
3508 $timings->startTiming();
3509
3510 $dirtyChunks = 0;
3511 for($xx = -1; $xx <= 1; ++$xx){
3512 for($zz = -1; $zz <= 1; ++$zz){
3513 $this->unregisterChunkLoader($temporaryChunkLoader, $x + $xx, $z + $zz);
3514 if(!$this->unlockChunk($x + $xx, $z + $zz, $chunkLockId)){
3515 $dirtyChunks++;
3516 }
3517 }
3518 }
3519
3520 $index = World::chunkHash($x, $z);
3521 if(!isset($this->activeChunkPopulationTasks[$index])){
3522 throw new AssumptionFailedError("This should always be set, regardless of whether the task was orphaned or not");
3523 }
3524 if(!$this->activeChunkPopulationTasks[$index]){
3525 $this->logger->debug("Discarding orphaned population result for chunk x=$x,z=$z");
3526 unset($this->activeChunkPopulationTasks[$index]);
3527 }else{
3528 if($dirtyChunks === 0){
3529 $oldChunk = $this->loadChunk($x, $z);
3530 $this->setChunk($x, $z, $chunk);
3531
3532 foreach($adjacentChunks as $relativeChunkHash => $adjacentChunk){
3533 World::getXZ($relativeChunkHash, $relativeX, $relativeZ);
3534 if($relativeX < -1 || $relativeX > 1 || $relativeZ < -1 || $relativeZ > 1){
3535 throw new AssumptionFailedError("Adjacent chunks should be in range -1 ... +1 coordinates");
3536 }
3537 $this->setChunk($x + $relativeX, $z + $relativeZ, $adjacentChunk);
3538 }
3539
3540 if(($oldChunk === null || !$oldChunk->isPopulated()) && $chunk->isPopulated()){
3541 if(ChunkPopulateEvent::hasHandlers()){
3542 (new ChunkPopulateEvent($this, $x, $z, $chunk))->call();
3543 }
3544
3545 foreach($this->getChunkListeners($x, $z) as $listener){
3546 $listener->onChunkPopulated($x, $z, $chunk);
3547 }
3548 }
3549 }else{
3550 $this->logger->debug("Discarding population result for chunk x=$x,z=$z - terrain was modified on the main thread before async population completed");
3551 }
3552
3553 //This needs to be in this specific spot because user code might call back to orderChunkPopulation().
3554 //If it does, and finds the promise, and doesn't find an active task associated with it, it will schedule
3555 //another PopulationTask. We don't want that because we're here processing the results.
3556 //We can't remove the promise from the array before setting the chunks in the world because that would lead
3557 //to the same problem. Therefore, it's necessary that this code be split into two if/else, with this in the
3558 //middle.
3559 unset($this->activeChunkPopulationTasks[$index]);
3560
3561 if($dirtyChunks === 0){
3562 $promise = $this->chunkPopulationRequestMap[$index] ?? null;
3563 if($promise !== null){
3564 unset($this->chunkPopulationRequestMap[$index]);
3565 $promise->resolve($chunk);
3566 }else{
3567 //Handlers of ChunkPopulateEvent, ChunkLoadEvent, or just ChunkListeners can cause this
3568 $this->logger->debug("Unable to resolve population promise for chunk x=$x,z=$z - populated chunk was forcibly unloaded while setting modified chunks");
3569 }
3570 }else{
3571 //request failed, stick it back on the queue
3572 //we didn't resolve the promise or touch it in any way, so any fake chunk loaders are still valid and
3573 //don't need to be added a second time.
3574 $this->addChunkHashToPopulationRequestQueue($index);
3575 }
3576
3577 $this->drainPopulationRequestQueue();
3578 }
3579 $timings->stopTiming();
3580 }
3581
3582 public function doChunkGarbageCollection() : void{
3583 $this->timings->doChunkGC->startTiming();
3584
3585 foreach($this->chunks as $index => $chunk){
3586 if(!isset($this->unloadQueue[$index])){
3587 World::getXZ($index, $X, $Z);
3588 if(!$this->isSpawnChunk($X, $Z)){
3589 $this->unloadChunkRequest($X, $Z, true);
3590 }
3591 }
3592 $chunk->collectGarbage();
3593 }
3594
3595 $this->provider->doGarbageCollection();
3596
3597 $this->timings->doChunkGC->stopTiming();
3598 }
3599
3600 public function unloadChunks(bool $force = false) : void{
3601 if(count($this->unloadQueue) > 0){
3602 $maxUnload = 96;
3603 $now = microtime(true);
3604 foreach($this->unloadQueue as $index => $time){
3605 World::getXZ($index, $X, $Z);
3606
3607 if(!$force){
3608 if($maxUnload <= 0){
3609 break;
3610 }elseif($time > ($now - 30)){
3611 continue;
3612 }
3613 }
3614
3615 //If the chunk can't be unloaded, it stays on the queue
3616 if($this->unloadChunk($X, $Z, true)){
3617 unset($this->unloadQueue[$index]);
3618 --$maxUnload;
3619 }
3620 }
3621 }
3622 }
3623}
pop(int $count=1)
Definition Item.php:431
getChunkListeners(int $chunkX, int $chunkZ)
Definition World.php:890
removeEntity(Entity $entity)
Definition World.php:2768
getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst=false)
Definition World.php:1504
getHighestAdjacentBlockLight(int $x, int $y, int $z)
Definition World.php:1883
setDisplayName(string $name)
Definition World.php:3254
getPotentialBlockSkyLightAt(int $x, int $y, int $z)
Definition World.php:1788
removeOnUnloadCallback(\Closure $callback)
Definition World.php:667
isChunkLocked(int $chunkX, int $chunkZ)
Definition World.php:2594
setSpawnLocation(Vector3 $pos)
Definition World.php:2720
getPotentialLightAt(int $x, int $y, int $z)
Definition World.php:1770
createBlockUpdatePackets(array $blocks)
Definition World.php:1090
getSafeSpawn(?Vector3 $spawn=null)
Definition World.php:3190
registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ)
Definition World.php:844
getBlockAt(int $x, int $y, int $z, bool $cached=true, bool $addToCache=true)
Definition World.php:1934
getChunkEntities(int $chunkX, int $chunkZ)
Definition World.php:2517
addEntity(Entity $entity)
Definition World.php:2734
getBlockLightAt(int $x, int $y, int $z)
Definition World.php:1813
getBlock(Vector3 $pos, bool $cached=true, bool $addToCache=true)
Definition World.php:1921
static chunkHash(int $x, int $z)
Definition World.php:384
broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet)
Definition World.php:794
getOrLoadChunkAtPosition(Vector3 $pos)
Definition World.php:2524
static chunkBlockHash(int $x, int $y, int $z)
Definition World.php:423
getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z)
Definition World.php:1868
getFullLight(Vector3 $pos)
Definition World.php:1727
isInWorld(int $x, int $y, int $z)
Definition World.php:1903
unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId)
Definition World.php:2579
getChunkLoaders(int $chunkX, int $chunkZ)
Definition World.php:777
getAdjacentChunks(int $x, int $z)
Definition World.php:2534
getChunkPlayers(int $chunkX, int $chunkZ)
Definition World.php:767
getTileAt(int $x, int $y, int $z)
Definition World.php:2474
getHighestAdjacentFullLightAt(int $x, int $y, int $z)
Definition World.php:1751
setChunkTickRadius(int $radius)
Definition World.php:1209
getViewersForPosition(Vector3 $pos)
Definition World.php:787
getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType=Entity::class, bool $includeDead=false)
Definition World.php:2417
requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader)
Definition World.php:3411
addSound(Vector3 $pos, Sound $sound, ?array $players=null)
Definition World.php:695
registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ)
Definition World.php:1228
setBlock(Vector3 $pos, Block $block, bool $update=true)
Definition World.php:1989
getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity=null)
Definition World.php:2385
static getXZ(int $hash, ?int &$x, ?int &$z)
Definition World.php:449
getBlockCollisionBoxes(AxisAlignedBB $bb)
Definition World.php:1634
getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity=null)
Definition World.php:2367
isSpawnChunk(int $X, int $Z)
Definition World.php:3148
useBreakOn(Vector3 $vector, ?Item &$item=null, ?Player $player=null, bool $createParticles=false, array &$returnedItems=[])
Definition World.php:2104
getPotentialLight(Vector3 $pos)
Definition World.php:1759
addParticle(Vector3 $pos, Particle $particle, ?array $players=null)
Definition World.php:724
unregisterChunkListenerFromAll(ChunkListener $listener)
Definition World.php:877
notifyNeighbourBlockUpdate(int $x, int $y, int $z)
Definition World.php:1493
loadChunk(int $x, int $z)
Definition World.php:2902
__construct(private Server $server, private string $folderName, private WritableWorldProvider $provider, private AsyncPool $workerPool)
Definition World.php:482
getHighestAdjacentPotentialLightAt(int $x, int $y, int $z)
Definition World.php:1778
setBlockAt(int $x, int $y, int $z, Block $block, bool $update=true)
Definition World.php:2001
useItemOn(Vector3 $vector, Item &$item, Facing $face, ?Vector3 $clickVector=null, ?Player $player=null, bool $playSound=false, array &$returnedItems=[])
Definition World.php:2210
unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ)
Definition World.php:1238
getRealBlockSkyLightAt(int $x, int $y, int $z)
Definition World.php:1803
static blockHash(int $x, int $y, int $z)
Definition World.php:403
getTile(Vector3 $pos)
Definition World.php:2467
getFullLightAt(int $x, int $y, int $z)
Definition World.php:1738
getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z)
Definition World.php:1876
orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader)
Definition World.php:3434
dropExperience(Vector3 $pos, int $amount)
Definition World.php:2081
isInLoadedTerrain(Vector3 $pos)
Definition World.php:2693
lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId)
Definition World.php:2562
getHighestBlockAt(int $x, int $z)
Definition World.php:2683
scheduleDelayedBlockUpdate(Vector3 $pos, int $delay)
Definition World.php:1466
static getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z)
Definition World.php:433
addOnUnloadCallback(\Closure $callback)
Definition World.php:662
requestSafeSpawn(?Vector3 $spawn=null)
Definition World.php:3163
unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ)
Definition World.php:861