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